diff options
Diffstat (limited to 'src/vs')
705 files changed, 15894 insertions, 9931 deletions
diff --git a/src/vs/base/browser/globalPointerMoveMonitor.ts b/src/vs/base/browser/globalPointerMoveMonitor.ts index fd3d53c362d..d637884a4c6 100644 --- a/src/vs/base/browser/globalPointerMoveMonitor.ts +++ b/src/vs/base/browser/globalPointerMoveMonitor.ts @@ -84,6 +84,8 @@ export class GlobalPointerMoveMonitor<R extends { buttons: number } = IPointerMo this._pointerMoveCallback = pointerMoveCallback; this._onStopCallback = onStopCallback; + let eventSource: Element | Window = initialElement; + try { initialElement.setPointerCapture(pointerId); this._hooks.add(toDisposable(() => { @@ -91,12 +93,18 @@ export class GlobalPointerMoveMonitor<R extends { buttons: number } = IPointerMo })); } catch (err) { // See https://github.com/microsoft/vscode/issues/144584 - // `setPointerCapture` sometimes fails when being invoked from a `mousedown` listener - // Things appear to work even with `setPointerCapture` failing, so no need to do anything special. + // See https://github.com/microsoft/vscode/issues/146947 + // `setPointerCapture` sometimes fails when being invoked + // from a `mousedown` listener on macOS and Windows + // and it always fails on Linux with the exception: + // DOMException: Failed to execute 'setPointerCapture' on 'Element': + // No active pointer with the given id is found. + // In case of failure, we bind the listeners on the window + eventSource = window; } this._hooks.add(dom.addDisposableThrottledListener<R, PointerEvent>( - initialElement, + eventSource, dom.EventType.POINTER_MOVE, (data: R) => { if (data.buttons !== initialButtons) { @@ -110,7 +118,7 @@ export class GlobalPointerMoveMonitor<R extends { buttons: number } = IPointerMo )); this._hooks.add(dom.addDisposableListener( - initialElement, + eventSource, dom.EventType.POINTER_UP, (e: PointerEvent) => this.stopMonitoring(true) )); diff --git a/src/vs/base/browser/ui/actionbar/actionViewItems.ts b/src/vs/base/browser/ui/actionbar/actionViewItems.ts index e63f8d77d65..42bbf152ec5 100644 --- a/src/vs/base/browser/ui/actionbar/actionViewItems.ts +++ b/src/vs/base/browser/ui/actionbar/actionViewItems.ts @@ -329,6 +329,7 @@ export class ActionViewItem extends BaseActionViewItem { if (title && this.label) { this.label.title = title; + this.label.setAttribute('aria-label', title); } } diff --git a/src/vs/base/browser/ui/actionbar/actionbar.ts b/src/vs/base/browser/ui/actionbar/actionbar.ts index a5355f86414..2a5548664c0 100644 --- a/src/vs/base/browser/ui/actionbar/actionbar.ts +++ b/src/vs/base/browser/ui/actionbar/actionbar.ts @@ -43,6 +43,7 @@ export interface IActionBarOptions { readonly actionViewItemProvider?: IActionViewItemProvider; readonly actionRunner?: IActionRunner; readonly ariaLabel?: string; + readonly ariaRole?: string; readonly animated?: boolean; readonly triggerKeys?: ActionTrigger; readonly allowContextMenu?: boolean; @@ -69,6 +70,7 @@ export class ActionBar extends Disposable implements IActionRunner { // View Items viewItems: IActionViewItem[]; + private viewItemDisposables: Map<IActionViewItem, IDisposable>; private previouslyFocusedItem?: number; protected focusedItem?: number; private focusTracker: DOM.IFocusTracker; @@ -118,6 +120,7 @@ export class ActionBar extends Disposable implements IActionRunner { this._actionIds = []; this.viewItems = []; + this.viewItemDisposables = new Map<IActionViewItem, IDisposable>(); this.focusedItem = undefined; this.domNode = document.createElement('div'); @@ -210,7 +213,7 @@ export class ActionBar extends Disposable implements IActionRunner { this.actionsList = document.createElement('ul'); this.actionsList.className = 'actions-container'; - this.actionsList.setAttribute('role', 'toolbar'); + this.actionsList.setAttribute('role', this.options.ariaRole || 'toolbar'); if (this.options.ariaLabel) { this.actionsList.setAttribute('aria-label', this.options.ariaLabel); @@ -223,7 +226,7 @@ export class ActionBar extends Disposable implements IActionRunner { private refreshRole(): void { if (this.length() >= 2) { - this.actionsList.setAttribute('role', 'toolbar'); + this.actionsList.setAttribute('role', this.options.ariaRole || 'toolbar'); } else { this.actionsList.setAttribute('role', 'presentation'); } @@ -317,13 +320,6 @@ export class ActionBar extends Disposable implements IActionRunner { actionViewItemElement.className = 'action-item'; actionViewItemElement.setAttribute('role', 'presentation'); - // Prevent native context menu on actions - if (!this.options.allowContextMenu) { - this._register(DOM.addDisposableListener(actionViewItemElement, DOM.EventType.CONTEXT_MENU, (e: DOM.EventLike) => { - DOM.EventHelper.stop(e, true); - })); - } - let item: IActionViewItem | undefined; if (this.options.actionViewItemProvider) { @@ -334,6 +330,13 @@ export class ActionBar extends Disposable implements IActionRunner { item = new ActionViewItem(this.context, action, options); } + // Prevent native context menu on actions + if (!this.options.allowContextMenu) { + this.viewItemDisposables.set(item, DOM.addDisposableListener(actionViewItemElement, DOM.EventType.CONTEXT_MENU, (e: DOM.EventLike) => { + DOM.EventHelper.stop(e, true); + })); + } + item.actionRunner = this._actionRunner; item.setActionContext(this.context); item.render(actionViewItemElement); @@ -386,6 +389,8 @@ export class ActionBar extends Disposable implements IActionRunner { pull(index: number): void { if (index >= 0 && index < this.viewItems.length) { this.actionsList.removeChild(this.actionsList.childNodes[index]); + this.viewItemDisposables.get(this.viewItems[index])?.dispose(); + this.viewItemDisposables.delete(this.viewItems[index]); dispose(this.viewItems.splice(index, 1)); this._actionIds.splice(index, 1); this.refreshRole(); @@ -394,6 +399,8 @@ export class ActionBar extends Disposable implements IActionRunner { clear(): void { dispose(this.viewItems); + this.viewItemDisposables.forEach(d => d.dispose()); + this.viewItemDisposables.clear(); this.viewItems = []; this._actionIds = []; DOM.clearNode(this.actionsList); @@ -463,7 +470,7 @@ export class ActionBar extends Disposable implements IActionRunner { this.focusedItem = (this.focusedItem + 1) % this.viewItems.length; item = this.viewItems[this.focusedItem]; - } while (this.focusedItem !== startIndex && this.options.focusOnlyEnabledItems && !item.isEnabled()); + } while (this.focusedItem !== startIndex && ((this.options.focusOnlyEnabledItems && !item.isEnabled()) || item.action.id === Separator.ID)); this.updateFocus(); return true; @@ -490,7 +497,7 @@ export class ActionBar extends Disposable implements IActionRunner { this.focusedItem = this.viewItems.length - 1; } item = this.viewItems[this.focusedItem]; - } while (this.focusedItem !== startIndex && this.options.focusOnlyEnabledItems && !item.isEnabled()); + } while (this.focusedItem !== startIndex && ((this.options.focusOnlyEnabledItems && !item.isEnabled()) || item.action.id === Separator.ID)); this.updateFocus(true); @@ -518,6 +525,10 @@ export class ActionBar extends Disposable implements IActionRunner { focusItem = false; } + if (actionViewItem.action.id === Separator.ID) { + focusItem = false; + } + if (!focusItem) { this.actionsList.focus({ preventScroll }); this.previouslyFocusedItem = undefined; diff --git a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf Binary files differindex 02beee38fd2..2f5dbfcc76d 100644 --- a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf +++ b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf diff --git a/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts b/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts index 79f89381904..0af76e3b6a0 100644 --- a/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts +++ b/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts @@ -88,6 +88,7 @@ export class DropdownMenuActionViewItem extends BaseActionViewItem { this.element.setAttribute('aria-haspopup', 'true'); this.element.setAttribute('aria-expanded', 'false'); this.element.title = this._action.label || ''; + this.element.ariaLabel = this._action.label || ''; return null; }; diff --git a/src/vs/base/browser/ui/iconLabel/iconLabel.ts b/src/vs/base/browser/ui/iconLabel/iconLabel.ts index b400a4fd83e..5e7ff8fa7de 100644 --- a/src/vs/base/browser/ui/iconLabel/iconLabel.ts +++ b/src/vs/base/browser/ui/iconLabel/iconLabel.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import 'vs/css!./iconlabel'; import * as dom from 'vs/base/browser/dom'; import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; @@ -11,7 +12,6 @@ import { IMatch } from 'vs/base/common/filters'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { equals } from 'vs/base/common/objects'; import { Range } from 'vs/base/common/range'; -import 'vs/css!./iconlabel'; export interface IIconLabelCreationOptions { supportHighlights?: boolean; diff --git a/src/vs/base/browser/ui/iconLabel/iconLabelHover.ts b/src/vs/base/browser/ui/iconLabel/iconLabelHover.ts index eb557f16f88..3034ea52d93 100644 --- a/src/vs/base/browser/ui/iconLabel/iconLabelHover.ts +++ b/src/vs/base/browser/ui/iconLabel/iconLabelHover.ts @@ -187,7 +187,12 @@ export function setupCustomHover(hoverDelegate: IHoverDelegate, htmlElement: HTM }; if (hoverDelegate.placement === undefined || hoverDelegate.placement === 'mouse') { // track the mouse position - const onMouseMove = (e: MouseEvent) => target.x = e.x + 10; + const onMouseMove = (e: MouseEvent) => { + target.x = e.x + 10; + if ((e.target instanceof HTMLElement) && e.target.classList.contains('action-label')) { + hideHover(true, true); + } + }; toDispose.add(dom.addDisposableListener(htmlElement, dom.EventType.MOUSE_MOVE, onMouseMove, true)); } toDispose.add(triggerShowHover(hoverDelegate.delay, false, target)); diff --git a/src/vs/base/browser/ui/toolbar/toolbar.ts b/src/vs/base/browser/ui/toolbar/toolbar.ts index 02c98734105..d5811906eda 100644 --- a/src/vs/base/browser/ui/toolbar/toolbar.ts +++ b/src/vs/base/browser/ui/toolbar/toolbar.ts @@ -28,6 +28,7 @@ export interface IToolBarOptions { anchorAlignmentProvider?: () => AnchorAlignment; renderDropdownAsChildElement?: boolean; moreIcon?: CSSIcon; + allowContextMenu?: boolean; } /** @@ -63,6 +64,7 @@ export class ToolBar extends Disposable { orientation: options.orientation, ariaLabel: options.ariaLabel, actionRunner: options.actionRunner, + allowContextMenu: options.allowContextMenu, actionViewItemProvider: (action: IAction) => { if (action.id === ToggleMenuAction.ID) { this.toggleMenuActionViewItem = new DropdownMenuActionViewItem( diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index 8b3aa20f4da..93362ef8184 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -630,7 +630,7 @@ class TypeFilter<T> implements ITreeFilter<T, FuzzyScore | LabelFuzzyScore>, IDi return { data: FuzzyScore.Default, visibility: true }; } - const score = fuzzyScore(this._pattern, this._lowercasePattern, 0, labelStr, labelStr.toLowerCase(), 0, true); + const score = fuzzyScore(this._pattern, this._lowercasePattern, 0, labelStr, labelStr.toLowerCase(), 0); if (score) { this._matchCount++; return labels.length === 1 ? diff --git a/src/vs/base/common/cache.ts b/src/vs/base/common/cache.ts index 5fac0d8f11b..1e675c36e43 100644 --- a/src/vs/base/common/cache.ts +++ b/src/vs/base/common/cache.ts @@ -41,19 +41,40 @@ export class Cache<T> { * Caches just the last value. * The key must be JSON serializable. */ -export class LRUCachedComputed<TArg, TComputed> { +export class LRUCachedFunction<TArg, TComputed> { private lastCache: TComputed | undefined = undefined; private lastArgKey: string | undefined = undefined; - constructor(private readonly computeFn: (arg: TArg) => TComputed) { + constructor(private readonly fn: (arg: TArg) => TComputed) { } public get(arg: TArg): TComputed { const key = JSON.stringify(arg); if (this.lastArgKey !== key) { this.lastArgKey = key; - this.lastCache = this.computeFn(arg); + this.lastCache = this.fn(arg); } return this.lastCache!; } } + +/** + * Uses an unbounded cache (referential equality) to memoize the results of the given function. +*/ +export class CachedFunction<TArg, TValue> { + private readonly _map = new Map<TArg, TValue>(); + public get cachedValues(): ReadonlyMap<TArg, TValue> { + return this._map; + } + + constructor(private readonly fn: (arg: TArg) => TValue) { } + + public get(arg: TArg): TValue { + if (this._map.has(arg)) { + return this._map.get(arg)!; + } + const value = this.fn(arg); + this._map.set(arg, value); + return value; + } +} diff --git a/src/vs/base/common/errorMessage.ts b/src/vs/base/common/errorMessage.ts index 18d5f770221..eca37716066 100644 --- a/src/vs/base/common/errorMessage.ts +++ b/src/vs/base/common/errorMessage.ts @@ -84,12 +84,8 @@ export function toErrorMessage(error: any = null, verbose: boolean = false): str } -export interface IErrorOptions { - actions?: readonly IAction[]; -} - -export interface IErrorWithActions { - actions?: readonly IAction[]; +export interface IErrorWithActions extends Error { + actions: IAction[]; } export function isErrorWithActions(obj: unknown): obj is IErrorWithActions { @@ -98,12 +94,15 @@ export function isErrorWithActions(obj: unknown): obj is IErrorWithActions { return candidate instanceof Error && Array.isArray(candidate.actions); } -export function createErrorWithActions(message: string, options: IErrorOptions = Object.create(null)): Error & IErrorWithActions { - const result = new Error(message); - - if (options.actions) { - (result as IErrorWithActions).actions = options.actions; +export function createErrorWithActions(messageOrError: string | Error, actions: IAction[]): IErrorWithActions { + let error: IErrorWithActions; + if (typeof messageOrError === 'string') { + error = new Error(messageOrError) as IErrorWithActions; + } else { + error = messageOrError as IErrorWithActions; } - return result; + error.actions = actions; + + return error; } diff --git a/src/vs/base/common/errors.ts b/src/vs/base/common/errors.ts index 08528474361..4f397866ae4 100644 --- a/src/vs/base/common/errors.ts +++ b/src/vs/base/common/errors.ts @@ -235,6 +235,10 @@ export class ExpectedError extends Error { export class ErrorNoTelemetry extends Error { public static fromError(err: any): ErrorNoTelemetry { + if (err && err instanceof ErrorNoTelemetry) { + return err; + } + if (err && err instanceof Error) { const result = new ErrorNoTelemetry(); result.name = err.name; @@ -248,3 +252,20 @@ export class ErrorNoTelemetry extends Error { readonly logTelemetry = false; } + +/** + * This error indicates a bug. + * Do not throw this for invalid user input. + * Only catch this error to recover gracefully from bugs. + */ +export class BugIndicatingError extends Error { + constructor(message: string) { + super(message); + Object.setPrototypeOf(this, BugIndicatingError.prototype); + + // Because we know for sure only buggy code throws this, + // we definitely want to break here and fix the bug. + // eslint-disable-next-line no-debugger + debugger; + } +} diff --git a/src/vs/base/common/extpath.ts b/src/vs/base/common/extpath.ts index d938a8184f3..a0fe8ad76a8 100644 --- a/src/vs/base/common/extpath.ts +++ b/src/vs/base/common/extpath.ts @@ -317,9 +317,8 @@ export function isRootOrDriveLetter(path: string): boolean { return pathNormalized === posix.sep; } -export function hasDriveLetter(path: string, continueAsWindows?: boolean): boolean { - const isWindowsPath: boolean = ((continueAsWindows !== undefined) ? continueAsWindows : isWindows); - if (isWindowsPath) { +export function hasDriveLetter(path: string, isWindowsOS: boolean = isWindows): boolean { + if (isWindowsOS) { return isWindowsDriveLetter(path.charCodeAt(0)) && path.charCodeAt(1) === CharCode.Colon; } diff --git a/src/vs/base/common/filters.ts b/src/vs/base/common/filters.ts index e1d674ff9a3..d0a8a09aa70 100644 --- a/src/vs/base/common/filters.ts +++ b/src/vs/base/common/filters.ts @@ -363,14 +363,14 @@ export function matchesFuzzy(word: string, wordToMatchAgainst: string, enableSep * powerful than `matchesFuzzy` */ export function matchesFuzzy2(pattern: string, word: string): IMatch[] | null { - const score = fuzzyScore(pattern, pattern.toLowerCase(), 0, word, word.toLowerCase(), 0, true); + const score = fuzzyScore(pattern, pattern.toLowerCase(), 0, word, word.toLowerCase(), 0, { firstMatchCanBeWeak: true, boostFullMatch: true }); return score ? createMatches(score) : null; } export function anyScore(pattern: string, lowPattern: string, patternPos: number, word: string, lowWord: string, wordPos: number): FuzzyScore { const max = Math.min(13, pattern.length); for (; patternPos < max; patternPos++) { - const result = fuzzyScore(pattern, lowPattern, patternPos, word, lowWord, wordPos, false); + const result = fuzzyScore(pattern, lowPattern, patternPos, word, lowWord, wordPos, { firstMatchCanBeWeak: false, boostFullMatch: true }); if (result) { return result; } @@ -472,8 +472,13 @@ function isSeparatorAtPos(value: string, index: number): boolean { case CharCode.Colon: case CharCode.DollarSign: case CharCode.LessThan: + case CharCode.GreaterThan: case CharCode.OpenParen: + case CharCode.CloseParen: case CharCode.OpenSquareBracket: + case CharCode.CloseSquareBracket: + case CharCode.OpenCurlyBrace: + case CharCode.CloseCurlyBrace: return true; case undefined: return false; @@ -541,11 +546,21 @@ export namespace FuzzyScore { } } +export abstract class FuzzyScoreOptions { + + static default = { boostFullMatch: true, firstMatchCanBeWeak: false }; + + constructor( + readonly firstMatchCanBeWeak: boolean, + readonly boostFullMatch: boolean, + ) { } +} + export interface FuzzyScorer { - (pattern: string, lowPattern: string, patternPos: number, word: string, lowWord: string, wordPos: number, firstMatchCanBeWeak: boolean): FuzzyScore | undefined; + (pattern: string, lowPattern: string, patternPos: number, word: string, lowWord: string, wordPos: number, options?: FuzzyScoreOptions): FuzzyScore | undefined; } -export function fuzzyScore(pattern: string, patternLow: string, patternStart: number, word: string, wordLow: string, wordStart: number, firstMatchCanBeWeak: boolean): FuzzyScore | undefined { +export function fuzzyScore(pattern: string, patternLow: string, patternStart: number, word: string, wordLow: string, wordStart: number, options: FuzzyScoreOptions = FuzzyScoreOptions.default): FuzzyScore | undefined { const patternLen = pattern.length > _maxLen ? _maxLen : pattern.length; const wordLen = word.length > _maxLen ? _maxLen : word.length; @@ -630,7 +645,7 @@ export function fuzzyScore(pattern: string, patternLow: string, patternStart: nu printTables(pattern, patternStart, word, wordStart); } - if (!hasStrongFirstMatch[0] && !firstMatchCanBeWeak) { + if (!hasStrongFirstMatch[0] && !options.firstMatchCanBeWeak) { return undefined; } @@ -684,7 +699,7 @@ export function fuzzyScore(pattern: string, patternLow: string, patternStart: nu result.push(column); } - if (wordLen === patternLen) { + if (wordLen === patternLen && options.boostFullMatch) { // the word matches the pattern with all characters! // giving the score a total match boost (to come up ahead other words) result[0] += 2; @@ -783,16 +798,16 @@ function _doScore( //#region --- graceful --- -export function fuzzyScoreGracefulAggressive(pattern: string, lowPattern: string, patternPos: number, word: string, lowWord: string, wordPos: number, firstMatchCanBeWeak: boolean): FuzzyScore | undefined { - return fuzzyScoreWithPermutations(pattern, lowPattern, patternPos, word, lowWord, wordPos, true, firstMatchCanBeWeak); +export function fuzzyScoreGracefulAggressive(pattern: string, lowPattern: string, patternPos: number, word: string, lowWord: string, wordPos: number, options?: FuzzyScoreOptions): FuzzyScore | undefined { + return fuzzyScoreWithPermutations(pattern, lowPattern, patternPos, word, lowWord, wordPos, true, options); } -export function fuzzyScoreGraceful(pattern: string, lowPattern: string, patternPos: number, word: string, lowWord: string, wordPos: number, firstMatchCanBeWeak: boolean): FuzzyScore | undefined { - return fuzzyScoreWithPermutations(pattern, lowPattern, patternPos, word, lowWord, wordPos, false, firstMatchCanBeWeak); +export function fuzzyScoreGraceful(pattern: string, lowPattern: string, patternPos: number, word: string, lowWord: string, wordPos: number, options?: FuzzyScoreOptions): FuzzyScore | undefined { + return fuzzyScoreWithPermutations(pattern, lowPattern, patternPos, word, lowWord, wordPos, false, options); } -function fuzzyScoreWithPermutations(pattern: string, lowPattern: string, patternPos: number, word: string, lowWord: string, wordPos: number, aggressive: boolean, firstMatchCanBeWeak: boolean): FuzzyScore | undefined { - let top = fuzzyScore(pattern, lowPattern, patternPos, word, lowWord, wordPos, firstMatchCanBeWeak); +function fuzzyScoreWithPermutations(pattern: string, lowPattern: string, patternPos: number, word: string, lowWord: string, wordPos: number, aggressive: boolean, options?: FuzzyScoreOptions): FuzzyScore | undefined { + let top = fuzzyScore(pattern, lowPattern, patternPos, word, lowWord, wordPos, options); if (top && !aggressive) { // when using the original pattern yield a result we` @@ -810,7 +825,7 @@ function fuzzyScoreWithPermutations(pattern: string, lowPattern: string, pattern for (let movingPatternPos = patternPos + 1; movingPatternPos < tries; movingPatternPos++) { const newPattern = nextTypoPermutation(pattern, movingPatternPos); if (newPattern) { - const candidate = fuzzyScore(newPattern, newPattern.toLowerCase(), patternPos, word, lowWord, wordPos, firstMatchCanBeWeak); + const candidate = fuzzyScore(newPattern, newPattern.toLowerCase(), patternPos, word, lowWord, wordPos, options); if (candidate) { candidate[0] -= 3; // permutation penalty if (!top || candidate[0] > top[0]) { diff --git a/src/vs/base/common/fuzzyScorer.ts b/src/vs/base/common/fuzzyScorer.ts index 53615f750c5..9db4898dd85 100644 --- a/src/vs/base/common/fuzzyScorer.ts +++ b/src/vs/base/common/fuzzyScorer.ts @@ -315,7 +315,7 @@ function doScoreFuzzy2Multiple(target: string, query: IPreparedQueryPiece[], pat } function doScoreFuzzy2Single(target: string, query: IPreparedQueryPiece, patternStart: number, wordStart: number): FuzzyScore2 { - const score = fuzzyScore(query.original, query.originalLowercase, patternStart, target, target.toLowerCase(), wordStart, true); + const score = fuzzyScore(query.original, query.originalLowercase, patternStart, target, target.toLowerCase(), wordStart); if (!score) { return NO_SCORE2; } diff --git a/src/vs/base/common/glob.ts b/src/vs/base/common/glob.ts index 3eb569d70e7..0a5039769f5 100644 --- a/src/vs/base/common/glob.ts +++ b/src/vs/base/common/glob.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { equals } from 'vs/base/common/arrays'; import { isThenable } from 'vs/base/common/async'; import { CharCode } from 'vs/base/common/charCode'; import { isEqualOrParent } from 'vs/base/common/extpath'; @@ -491,7 +492,7 @@ function toRegExp(pattern: string): ParsedStringPattern { /** * Simplified glob matching. Supports a subset of glob patterns: - * * `*` to match one or more characters in a path segment + * * `*` to match zero or more characters in a path segment * * `?` to match on one character in a path segment * * `**` to match any number of path segments, including none * * `{}` to group conditions (e.g. *.{ts,js} matches all TypeScript and JavaScript files) @@ -510,7 +511,7 @@ export function match(arg1: string | IExpression | IRelativePattern, path: strin /** * Simplified glob matching. Supports a subset of glob patterns: - * * `*` to match one or more characters in a path segment + * * `*` to match zero or more characters in a path segment * * `?` to match on one character in a path segment * * `**` to match any number of path segments, including none * * `{}` to group conditions (e.g. *.{ts,js} matches all TypeScript and JavaScript files) @@ -584,13 +585,38 @@ function parsedExpression(expression: IExpression, options: IGlobOptions): Parse } const resultExpression: ParsedStringPattern = function (path: string, basename?: string) { - for (let i = 0, n = parsedPatterns.length; i < n; i++) { + let resultPromises: Promise<string | null>[] | undefined = undefined; - // Check if pattern matches path + for (let i = 0, n = parsedPatterns.length; i < n; i++) { const result = parsedPatterns[i](path, basename); - if (result) { - return result; + if (typeof result === 'string') { + return result; // immediately return as soon as the first expression matches } + + // If the result is a promise, we have to keep it for + // later processing and await the result properly. + if (isThenable(result)) { + if (!resultPromises) { + resultPromises = []; + } + + resultPromises.push(result); + } + } + + // With result promises, we have to loop over each and + // await the result before we can return any result. + if (resultPromises) { + return (async () => { + for (const resultPromise of resultPromises) { + const result = await resultPromise; + if (typeof result === 'string') { + return result; + } + } + + return null; + })(); } return null; @@ -611,6 +637,7 @@ function parsedExpression(expression: IExpression, options: IGlobOptions): Parse const resultExpression: ParsedStringPattern = function (path: string, base?: string, hasSibling?: (name: string) => boolean | Promise<boolean>) { let name: string | undefined = undefined; + let resultPromises: Promise<string | null>[] | undefined = undefined; for (let i = 0, n = parsedPatterns.length; i < n; i++) { @@ -627,11 +654,36 @@ function parsedExpression(expression: IExpression, options: IGlobOptions): Parse } const result = parsedPattern(path, base, name, hasSibling); - if (result) { - return result; + if (typeof result === 'string') { + return result; // immediately return as soon as the first expression matches + } + + // If the result is a promise, we have to keep it for + // later processing and await the result properly. + if (isThenable(result)) { + if (!resultPromises) { + resultPromises = []; + } + + resultPromises.push(result); } } + // With result promises, we have to loop over each and + // await the result before we can return any result. + if (resultPromises) { + return (async () => { + for (const resultPromise of resultPromises) { + const result = await resultPromise; + if (typeof result === 'string') { + return result; + } + } + + return null; + })(); + } + return null; }; @@ -746,3 +798,17 @@ function aggregateBasenameMatches(parsedPatterns: Array<ParsedStringPattern | Pa return aggregatedPatterns; } + +export function patternsEquals(patternsA: Array<string | IRelativePattern> | undefined, patternsB: Array<string | IRelativePattern> | undefined): boolean { + return equals(patternsA, patternsB, (a, b) => { + if (typeof a === 'string' && typeof b === 'string') { + return a === b; + } + + if (typeof a !== 'string' && typeof b !== 'string') { + return a.base === b.base && a.pattern === b.pattern; + } + + return false; + }); +} diff --git a/src/vs/base/common/iterator.ts b/src/vs/base/common/iterator.ts index 4e325694222..2771802f1de 100644 --- a/src/vs/base/common/iterator.ts +++ b/src/vs/base/common/iterator.ts @@ -92,6 +92,13 @@ export namespace Iterable { return value; } + export function forEach<T>(iterable: Iterable<T>, fn: (t: T, index: number) => any): void { + let index = 0; + for (const element of iterable) { + fn(element, index++); + } + } + /** * Returns an iterable slice of the array, with the same semantics as `array.slice()`. */ @@ -138,6 +145,14 @@ export namespace Iterable { } /** + * Consumes `atMost` elements from iterable and returns the consumed elements, + * and an iterable for the rest of the elements. + */ + export function collect<T>(iterable: Iterable<T>): T[] { + return consume(iterable)[0]; + } + + /** * Returns whether the iterables are the same length and all items are * equal using the comparator function. */ diff --git a/src/vs/base/common/labels.ts b/src/vs/base/common/labels.ts index 70ee48e82a0..19a6d4f46bc 100644 --- a/src/vs/base/common/labels.ts +++ b/src/vs/base/common/labels.ts @@ -3,72 +3,140 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { hasDriveLetter, isRootOrDriveLetter } from 'vs/base/common/extpath'; +import { firstOrDefault } from 'vs/base/common/arrays'; +import { hasDriveLetter, isRootOrDriveLetter, toSlashes } from 'vs/base/common/extpath'; import { Schemas } from 'vs/base/common/network'; -import { normalize, posix, sep, win32 } from 'vs/base/common/path'; -import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; -import { basename, isEqual, relativePath } from 'vs/base/common/resources'; +import { posix, sep, win32 } from 'vs/base/common/path'; +import { isMacintosh, isWindows, OperatingSystem, OS } from 'vs/base/common/platform'; +import { basename, extUri, extUriIgnorePathCase } from 'vs/base/common/resources'; import { rtrim, startsWithIgnoreCase } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; -export interface IWorkspaceFolderProvider { +export interface IPathLabelFormatting { + + /** + * The OS the path label is from to produce a label + * that matches OS expectations. + */ + readonly os: OperatingSystem; + + /** + * Whether to add a `~` when the path is in the + * user home directory. + * + * Note: this only applies to Linux, macOS but not + * Windows. + */ + readonly tildify?: IUserHomeProvider; + + /** + * Whether to convert to a relative path if the path + * is within any of the opened workspace folders. + */ + readonly relative?: IRelativePathProvider; +} + +export interface IRelativePathProvider { + + /** + * Whether to not add a prefix when in multi-root workspace. + */ + readonly noPrefix?: boolean; + + getWorkspace(): { folders: { uri: URI; name?: string }[] }; getWorkspaceFolder(resource: URI): { uri: URI; name?: string } | null; - getWorkspace(): { - folders: { uri: URI; name?: string }[]; - }; } export interface IUserHomeProvider { - userHome?: URI; + userHome: URI; } -/** - * @deprecated use LabelService instead - */ -export function getPathLabel(resource: URI | string, userHomeProvider?: IUserHomeProvider, rootProvider?: IWorkspaceFolderProvider): string { - if (typeof resource === 'string') { - resource = URI.file(resource); - } +export function getPathLabel(resource: URI, formatting: IPathLabelFormatting): string { + const { os, tildify: tildifier, relative: relatifier } = formatting; - // return early if we can resolve a relative path label from the root - if (rootProvider) { - const baseResource = rootProvider.getWorkspaceFolder(resource); - if (baseResource) { - const hasMultipleRoots = rootProvider.getWorkspace().folders.length > 1; - - let pathLabel: string; - if (isEqual(baseResource.uri, resource)) { - pathLabel = ''; // no label if paths are identical - } else { - pathLabel = relativePath(baseResource.uri, resource)!; - } + // return early with a relative path if we can resolve one + if (relatifier) { + const relativePath = getRelativePathLabel(resource, relatifier, os); + if (typeof relativePath === 'string') { + return relativePath; + } + } - if (hasMultipleRoots) { - const rootName = baseResource.name ? baseResource.name : basename(baseResource.uri); - pathLabel = pathLabel ? (rootName + ' • ' + pathLabel) : rootName; // always show root basename if there are multiple - } + // otherwise try to resolve a absolute path label and + // apply target OS standard path separators if target + // OS differs from actual OS we are running in + let absolutePath = resource.fsPath; + if (os === OperatingSystem.Windows && !isWindows) { + absolutePath = absolutePath.replace(/\//g, '\\'); + } else if (os !== OperatingSystem.Windows && isWindows) { + absolutePath = absolutePath.replace(/\\/g, '/'); + } - return pathLabel; + // macOS/Linux: tildify with provided user home directory + if (os !== OperatingSystem.Windows && tildifier?.userHome) { + let userHome = tildifier.userHome.fsPath; + + // This is a bit of a hack, but in order to figure out if the + // resource is in the user home, we need to make sure to convert it + // to a user home resource. We cannot assume that the resource is + // already a user home resource. + let userHomeCandidate: string; + if (resource.scheme !== tildifier.userHome.scheme && resource.path.startsWith(posix.sep)) { + userHomeCandidate = tildifier.userHome.with({ path: resource.path }).fsPath; + } else { + userHomeCandidate = resource.fsPath; } + + absolutePath = tildify(userHomeCandidate, userHome, os); } - // return if the resource is neither file:// nor untitled:// and no baseResource was provided - if (resource.scheme !== Schemas.file && resource.scheme !== Schemas.untitled) { - return resource.with({ query: null, fragment: null }).toString(true); + // normalize + const pathLib = os === OperatingSystem.Windows ? win32 : posix; + return pathLib.normalize(normalizeDriveLetter(absolutePath, os === OperatingSystem.Windows)); +} + +function getRelativePathLabel(resource: URI, relativePathProvider: IRelativePathProvider, os: OperatingSystem): string | undefined { + const pathLib = os === OperatingSystem.Windows ? win32 : posix; + const extUriLib = os === OperatingSystem.Linux ? extUri : extUriIgnorePathCase; + + const workspace = relativePathProvider.getWorkspace(); + const firstFolder = firstOrDefault(workspace.folders); + if (!firstFolder) { + return undefined; } - // convert c:\something => C:\something - if (hasDriveLetter(resource.fsPath)) { - return normalize(normalizeDriveLetter(resource.fsPath)); + // This is a bit of a hack, but in order to figure out the folder + // the resource belongs to, we need to make sure to convert it + // to a workspace resource. We cannot assume that the resource is + // already matching the workspace. + if (resource.scheme !== firstFolder.uri.scheme && resource.path.startsWith(posix.sep)) { + resource = firstFolder.uri.with({ path: resource.path }); } - // normalize and tildify (macOS, Linux only) - let res = normalize(resource.fsPath); - if (!isWindows && userHomeProvider?.userHome) { - res = tildify(res, userHomeProvider.userHome.fsPath); + const folder = relativePathProvider.getWorkspaceFolder(resource); + if (!folder) { + return undefined; + } + + let relativePathLabel: string | undefined = undefined; + if (extUriLib.isEqual(folder.uri, resource)) { + relativePathLabel = ''; // no label if paths are identical + } else { + relativePathLabel = extUriLib.relativePath(folder.uri, resource) ?? ''; } - return res; + // normalize + if (relativePathLabel) { + relativePathLabel = pathLib.normalize(relativePathLabel); + } + + // always show root basename if there are multiple folders + if (workspace.folders.length > 1 && !relativePathProvider.noPrefix) { + const rootName = folder.name ? folder.name : extUriLib.basenameOrAuthority(folder.uri); + relativePathLabel = relativePathLabel ? `${rootName} • ${relativePathLabel}` : rootName; + } + + return relativePathLabel; } export function getBaseLabel(resource: URI | string): string; @@ -92,8 +160,8 @@ export function getBaseLabel(resource: URI | string | undefined): string | undef return base; } -export function normalizeDriveLetter(path: string, continueAsWindows?: boolean): string { - if (hasDriveLetter(path, continueAsWindows)) { +export function normalizeDriveLetter(path: string, isWindowsOS: boolean = isWindows): string { + if (hasDriveLetter(path, isWindowsOS)) { return path.charAt(0).toUpperCase() + path.slice(1); } @@ -101,21 +169,29 @@ export function normalizeDriveLetter(path: string, continueAsWindows?: boolean): } let normalizedUserHomeCached: { original: string; normalized: string } = Object.create(null); -export function tildify(path: string, userHome: string): string { - if (isWindows || !path || !userHome) { - return path; // unsupported +export function tildify(path: string, userHome: string, os = OS): string { + if (os === OperatingSystem.Windows || !path || !userHome) { + return path; // unsupported on Windows } - // Keep a normalized user home path as cache to prevent accumulated string creation let normalizedUserHome = normalizedUserHomeCached.original === userHome ? normalizedUserHomeCached.normalized : undefined; if (!normalizedUserHome) { - normalizedUserHome = `${rtrim(userHome, posix.sep)}${posix.sep}`; + normalizedUserHome = userHome; + if (isWindows) { + normalizedUserHome = toSlashes(normalizedUserHome); // make sure that the path is POSIX normalized on Windows + } + normalizedUserHome = `${rtrim(normalizedUserHome, posix.sep)}${posix.sep}`; normalizedUserHomeCached = { original: userHome, normalized: normalizedUserHome }; } + let normalizedPath = path; + if (isWindows) { + normalizedPath = toSlashes(normalizedPath); // make sure that the path is POSIX normalized on Windows + } + // Linux: case sensitive, macOS: case insensitive - if (isLinux ? path.startsWith(normalizedUserHome) : startsWithIgnoreCase(path, normalizedUserHome)) { - path = `~/${path.substr(normalizedUserHome.length)}`; + if (os === OperatingSystem.Linux ? normalizedPath.startsWith(normalizedUserHome) : startsWithIgnoreCase(normalizedPath, normalizedUserHome)) { + return `~/${normalizedPath.substr(normalizedUserHome.length)}`; } return path; @@ -163,15 +239,15 @@ export function shorten(paths: string[], pathSeparator: string = sep): string[] // for every path let match = false; for (let pathIndex = 0; pathIndex < paths.length; pathIndex++) { - let path = paths[pathIndex]; + let originalPath = paths[pathIndex]; - if (path === '') { + if (originalPath === '') { shortenedPaths[pathIndex] = `.${pathSeparator}`; continue; } - if (!path) { - shortenedPaths[pathIndex] = path; + if (!originalPath) { + shortenedPaths[pathIndex] = originalPath; continue; } @@ -179,19 +255,20 @@ export function shorten(paths: string[], pathSeparator: string = sep): string[] // trim for now and concatenate unc path (e.g. \\network) or root path (/etc, ~/etc) later let prefix = ''; - if (path.indexOf(unc) === 0) { - prefix = path.substr(0, path.indexOf(unc) + unc.length); - path = path.substr(path.indexOf(unc) + unc.length); - } else if (path.indexOf(pathSeparator) === 0) { - prefix = path.substr(0, path.indexOf(pathSeparator) + pathSeparator.length); - path = path.substr(path.indexOf(pathSeparator) + pathSeparator.length); - } else if (path.indexOf(home) === 0) { - prefix = path.substr(0, path.indexOf(home) + home.length); - path = path.substr(path.indexOf(home) + home.length); + let trimmedPath = originalPath; + if (trimmedPath.indexOf(unc) === 0) { + prefix = trimmedPath.substr(0, trimmedPath.indexOf(unc) + unc.length); + trimmedPath = trimmedPath.substr(trimmedPath.indexOf(unc) + unc.length); + } else if (trimmedPath.indexOf(pathSeparator) === 0) { + prefix = trimmedPath.substr(0, trimmedPath.indexOf(pathSeparator) + pathSeparator.length); + trimmedPath = trimmedPath.substr(trimmedPath.indexOf(pathSeparator) + pathSeparator.length); + } else if (trimmedPath.indexOf(home) === 0) { + prefix = trimmedPath.substr(0, trimmedPath.indexOf(home) + home.length); + trimmedPath = trimmedPath.substr(trimmedPath.indexOf(home) + home.length); } // pick the first shortest subpath found - const segments: string[] = path.split(pathSeparator); + const segments: string[] = trimmedPath.split(pathSeparator); for (let subpathLength = 1; match && subpathLength <= segments.length; subpathLength++) { for (let start = segments.length - subpathLength; match && start >= 0; start--) { match = false; @@ -251,7 +328,7 @@ export function shorten(paths: string[], pathSeparator: string = sep): string[] } if (match) { - shortenedPaths[pathIndex] = path; // use full path if no unique subpaths found + shortenedPaths[pathIndex] = originalPath; // use original path if no unique subpaths found } } diff --git a/src/vs/base/common/map.ts b/src/vs/base/common/map.ts index c7d1b418241..78c23bc80b4 100644 --- a/src/vs/base/common/map.ts +++ b/src/vs/base/common/map.ts @@ -199,7 +199,9 @@ export class UriIterator implements IKeyIterator<URI> { private _states: UriIteratorState[] = []; private _stateIdx: number = 0; - constructor(private readonly _ignorePathCasing: (uri: URI) => boolean) { } + constructor( + private readonly _ignorePathCasing: (uri: URI) => boolean, + private readonly _ignoreQueryAndFragment: (uri: URI) => boolean) { } reset(key: URI): this { this._value = key; @@ -217,11 +219,13 @@ export class UriIterator implements IKeyIterator<URI> { this._states.push(UriIteratorState.Path); } } - if (this._value.query) { - this._states.push(UriIteratorState.Query); - } - if (this._value.fragment) { - this._states.push(UriIteratorState.Fragment); + if (!this._ignoreQueryAndFragment(key)) { + if (this._value.query) { + this._states.push(UriIteratorState.Query); + } + if (this._value.fragment) { + this._states.push(UriIteratorState.Fragment); + } } this._stateIdx = 0; return this; @@ -328,8 +332,8 @@ const enum Dir { export class TernarySearchTree<K, V> { - static forUris<E>(ignorePathCasing: (key: URI) => boolean = () => false): TernarySearchTree<URI, E> { - return new TernarySearchTree<URI, E>(new UriIterator(ignorePathCasing)); + static forUris<E>(ignorePathCasing: (key: URI) => boolean = () => false, ignoreQueryAndFragment: (key: URI) => boolean = () => false): TernarySearchTree<URI, E> { + return new TernarySearchTree<URI, E>(new UriIterator(ignorePathCasing, ignoreQueryAndFragment)); } static forPaths<E>(ignorePathCasing = false): TernarySearchTree<string, E> { diff --git a/src/vs/base/common/platform.ts b/src/vs/base/common/platform.ts index b2e6fef5f92..e0ea1d62c64 100644 --- a/src/vs/base/common/platform.ts +++ b/src/vs/base/common/platform.ts @@ -43,7 +43,6 @@ export interface INodeProcess { versions?: { electron?: string; }; - sandboxed?: boolean; type?: string; cwd: () => string; } @@ -65,7 +64,6 @@ if (typeof globals.vscode !== 'undefined' && typeof globals.vscode.process !== ' const isElectronProcess = typeof nodeProcess?.versions?.electron === 'string'; const isElectronRenderer = isElectronProcess && nodeProcess?.type === 'renderer'; -export const isElectronSandboxed = isElectronRenderer && nodeProcess?.sandboxed; interface INavigator { userAgent: string; diff --git a/src/vs/base/common/processes.ts b/src/vs/base/common/processes.ts index db23e105808..3e5c8b7e134 100644 --- a/src/vs/base/common/processes.ts +++ b/src/vs/base/common/processes.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IProcessEnvironment } from 'vs/base/common/platform'; +import { IProcessEnvironment, isLinux, isMacintosh } from 'vs/base/common/platform'; /** * Options to be passed to the external program or shell. @@ -124,3 +124,32 @@ export function sanitizeProcessEnvironment(env: IProcessEnvironment, ...preserve } }); } + +/** + * Remove dangerous environment variables that have caused crashes + * in forked processes (i.e. in ELECTRON_RUN_AS_NODE processes) + * + * @param env The env object to change + */ +export function removeDangerousEnvVariables(env: IProcessEnvironment | undefined): void { + if (!env) { + return; + } + + // Unset `DEBUG`, as an invalid value might lead to process crashes + // See https://github.com/microsoft/vscode/issues/130072 + delete env['DEBUG']; + + if (isMacintosh) { + // Unset `DYLD_LIBRARY_PATH`, as it leads to process crashes + // See https://github.com/microsoft/vscode/issues/104525 + // See https://github.com/microsoft/vscode/issues/105848 + delete env['DYLD_LIBRARY_PATH']; + } + + if (isLinux) { + // Unset `LD_PRELOAD`, as it might lead to process crashes + // See https://github.com/microsoft/vscode/issues/134177 + delete env['LD_PRELOAD']; + } +} diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index fd05d5a160c..c66cf41aa6c 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -41,6 +41,7 @@ export interface IProductConfiguration { readonly win32AppUserModelId?: string; readonly win32MutexName?: string; + readonly win32RegValueName?: string; readonly applicationName: string; readonly embedderIdentifier?: string; diff --git a/src/vs/base/common/strings.ts b/src/vs/base/common/strings.ts index 0037912f68a..62fddd69f91 100644 --- a/src/vs/base/common/strings.ts +++ b/src/vs/base/common/strings.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { LRUCachedComputed } from 'vs/base/common/cache'; +import { LRUCachedFunction } from 'vs/base/common/cache'; import { CharCode } from 'vs/base/common/charCode'; import { Lazy } from 'vs/base/common/lazy'; import { Constants } from 'vs/base/common/uint'; @@ -274,6 +274,29 @@ export function lastNonWhitespaceIndex(str: string, startIndex: number = str.len return -1; } +/** + * Function that works identically to String.prototype.replace, except, the + * replace function is allowed to be async and return a Promise. + */ +export function replaceAsync(str: string, search: RegExp, replacer: (match: string, ...args: any[]) => Promise<string>): Promise<string> { + let parts: (string | Promise<string>)[] = []; + + let last = 0; + for (const match of str.matchAll(search)) { + parts.push(str.slice(last, match.index)); + if (match.index === undefined) { + throw new Error('match.index should be defined'); + } + + last = match.index + match[0].length; + parts.push(replacer(match[0], ...match.slice(1), match.index, str, match.groups)); + } + + parts.push(str.slice(last)); + + return Promise.all(parts).then(p => p.join('')); +} + export function compare(a: string, b: string): number { if (a < b) { return -1; @@ -1052,7 +1075,7 @@ export class AmbiguousCharacters { ); }); - private static readonly cache = new LRUCachedComputed< + private static readonly cache = new LRUCachedFunction< string[], AmbiguousCharacters >((locales) => { diff --git a/src/vs/base/common/uuid.ts b/src/vs/base/common/uuid.ts index 75f89924655..0bd0c937cae 100644 --- a/src/vs/base/common/uuid.ts +++ b/src/vs/base/common/uuid.ts @@ -10,61 +10,72 @@ export function isUUID(value: string): boolean { return _UUIDPattern.test(value); } -// prep-work -const _data = new Uint8Array(16); -const _hex: string[] = []; -for (let i = 0; i < 256; i++) { - _hex.push(i.toString(16).padStart(2, '0')); -} +declare const crypto: undefined | { + //https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues#browser_compatibility + getRandomValues?(data: Uint8Array): Uint8Array; + //https://developer.mozilla.org/en-US/docs/Web/API/Crypto/randomUUID#browser_compatibility + randomUUID?(): string; +}; -// todo@jrieken - with node@15 crypto#getRandomBytes is available everywhere, https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues#browser_compatibility -let _fillRandomValues: (bucket: Uint8Array) => Uint8Array; +export const generateUuid = (function (): () => string { -declare const crypto: undefined | { getRandomValues(data: Uint8Array): Uint8Array }; + // use `randomUUID` if possible + if (typeof crypto === 'object' && typeof crypto.randomUUID === 'function') { + return crypto.randomUUID.bind(crypto); + } -if (typeof crypto === 'object' && typeof crypto.getRandomValues === 'function') { - // browser - _fillRandomValues = crypto.getRandomValues.bind(crypto); + // use `randomValues` if possible + let getRandomValues: (bucket: Uint8Array) => Uint8Array; + if (typeof crypto === 'object' && typeof crypto.getRandomValues === 'function') { + getRandomValues = crypto.getRandomValues.bind(crypto); -} else { - _fillRandomValues = function (bucket: Uint8Array): Uint8Array { - for (let i = 0; i < bucket.length; i++) { - bucket[i] = Math.floor(Math.random() * 256); - } - return bucket; - }; -} + } else { + getRandomValues = function (bucket: Uint8Array): Uint8Array { + for (let i = 0; i < bucket.length; i++) { + bucket[i] = Math.floor(Math.random() * 256); + } + return bucket; + }; + } -export function generateUuid(): string { - // get data - _fillRandomValues(_data); + // prep-work + const _data = new Uint8Array(16); + const _hex: string[] = []; + for (let i = 0; i < 256; i++) { + _hex.push(i.toString(16).padStart(2, '0')); + } - // set version bits - _data[6] = (_data[6] & 0x0f) | 0x40; - _data[8] = (_data[8] & 0x3f) | 0x80; + return function generateUuid(): string { + // get data + getRandomValues(_data); - // print as string - let i = 0; - let result = ''; - result += _hex[_data[i++]]; - result += _hex[_data[i++]]; - result += _hex[_data[i++]]; - result += _hex[_data[i++]]; - result += '-'; - result += _hex[_data[i++]]; - result += _hex[_data[i++]]; - result += '-'; - result += _hex[_data[i++]]; - result += _hex[_data[i++]]; - result += '-'; - result += _hex[_data[i++]]; - result += _hex[_data[i++]]; - result += '-'; - result += _hex[_data[i++]]; - result += _hex[_data[i++]]; - result += _hex[_data[i++]]; - result += _hex[_data[i++]]; - result += _hex[_data[i++]]; - result += _hex[_data[i++]]; - return result; -} + // set version bits + _data[6] = (_data[6] & 0x0f) | 0x40; + _data[8] = (_data[8] & 0x3f) | 0x80; + + // print as string + let i = 0; + let result = ''; + result += _hex[_data[i++]]; + result += _hex[_data[i++]]; + result += _hex[_data[i++]]; + result += _hex[_data[i++]]; + result += '-'; + result += _hex[_data[i++]]; + result += _hex[_data[i++]]; + result += '-'; + result += _hex[_data[i++]]; + result += _hex[_data[i++]]; + result += '-'; + result += _hex[_data[i++]]; + result += _hex[_data[i++]]; + result += '-'; + result += _hex[_data[i++]]; + result += _hex[_data[i++]]; + result += _hex[_data[i++]]; + result += _hex[_data[i++]]; + result += _hex[_data[i++]]; + result += _hex[_data[i++]]; + return result; + }; +})(); diff --git a/src/vs/base/node/processes.ts b/src/vs/base/node/processes.ts index 4085e82397c..c751a692473 100644 --- a/src/vs/base/node/processes.ts +++ b/src/vs/base/node/processes.ts @@ -87,35 +87,6 @@ function terminateProcess(process: cp.ChildProcess, cwd?: string): Promise<Termi return Promise.resolve({ success: true }); } -/** - * Remove dangerous environment variables that have caused crashes - * in forked processes (i.e. in ELECTRON_RUN_AS_NODE processes) - * - * @param env The env object to change - */ -export function removeDangerousEnvVariables(env: NodeJS.ProcessEnv | undefined): void { - if (!env) { - return; - } - - // Unset `DEBUG`, as an invalid value might lead to process crashes - // See https://github.com/microsoft/vscode/issues/130072 - delete env['DEBUG']; - - if (Platform.isMacintosh) { - // Unset `DYLD_LIBRARY_PATH`, as it leads to process crashes - // See https://github.com/microsoft/vscode/issues/104525 - // See https://github.com/microsoft/vscode/issues/105848 - delete env['DYLD_LIBRARY_PATH']; - } - - if (Platform.isLinux) { - // Unset `LD_PRELOAD`, as it might lead to process crashes - // See https://github.com/microsoft/vscode/issues/134177 - delete env['LD_PRELOAD']; - } -} - export function getWindowsShell(env = process.env as Platform.IProcessEnvironment): string { return env['comspec'] || 'cmd.exe'; } diff --git a/src/vs/base/parts/contextmenu/electron-main/contextmenu.ts b/src/vs/base/parts/contextmenu/electron-main/contextmenu.ts index fa64089c460..6b2eb1cb820 100644 --- a/src/vs/base/parts/contextmenu/electron-main/contextmenu.ts +++ b/src/vs/base/parts/contextmenu/electron-main/contextmenu.ts @@ -3,12 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BrowserWindow, ipcMain, IpcMainEvent, Menu, MenuItem } from 'electron'; +import { BrowserWindow, IpcMainEvent, Menu, MenuItem } from 'electron'; +import { validatedIpcMain } from 'vs/base/parts/ipc/electron-main/ipcMain'; import { withNullAsUndefined } from 'vs/base/common/types'; import { CONTEXT_MENU_CHANNEL, CONTEXT_MENU_CLOSE_CHANNEL, IPopupOptions, ISerializableContextMenuItem } from 'vs/base/parts/contextmenu/common/contextmenu'; export function registerContextMenuListener(): void { - ipcMain.on(CONTEXT_MENU_CHANNEL, (event: IpcMainEvent, contextMenuId: number, items: ISerializableContextMenuItem[], onClickChannel: string, options?: IPopupOptions) => { + validatedIpcMain.on(CONTEXT_MENU_CHANNEL, (event: IpcMainEvent, contextMenuId: number, items: ISerializableContextMenuItem[], onClickChannel: string, options?: IPopupOptions) => { const menu = createMenu(event, onClickChannel, items); menu.popup({ diff --git a/src/vs/base/parts/ipc/electron-main/ipc.electron.ts b/src/vs/base/parts/ipc/electron-main/ipc.electron.ts index 431c44bf3d1..f0d395c4bfa 100644 --- a/src/vs/base/parts/ipc/electron-main/ipc.electron.ts +++ b/src/vs/base/parts/ipc/electron-main/ipc.electron.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ipcMain, WebContents } from 'electron'; +import { WebContents } from 'electron'; +import { validatedIpcMain } from 'vs/base/parts/ipc/electron-main/ipcMain'; import { VSBuffer } from 'vs/base/common/buffer'; import { Emitter, Event } from 'vs/base/common/event'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; @@ -16,7 +17,7 @@ interface IIPCEvent { } function createScopedOnMessageEvent(senderId: number, eventName: string): Event<VSBuffer | null> { - const onMessage = Event.fromNodeEventEmitter<IIPCEvent>(ipcMain, eventName, (event, message) => ({ event, message })); + const onMessage = Event.fromNodeEventEmitter<IIPCEvent>(validatedIpcMain, eventName, (event, message) => ({ event, message })); const onMessageFromSender = Event.filter(onMessage, ({ event }) => event.sender.id === senderId); return Event.map(onMessageFromSender, ({ message }) => message ? VSBuffer.wrap(message) : message); @@ -30,7 +31,7 @@ export class Server extends IPCServer { private static readonly Clients = new Map<number, IDisposable>(); private static getOnDidClientConnect(): Event<ClientConnectionEvent> { - const onHello = Event.fromNodeEventEmitter<WebContents>(ipcMain, 'vscode:hello', ({ sender }) => sender); + const onHello = Event.fromNodeEventEmitter<WebContents>(validatedIpcMain, 'vscode:hello', ({ sender }) => sender); return Event.map(onHello, webContents => { const id = webContents.id; diff --git a/src/vs/base/parts/ipc/electron-main/ipc.mp.ts b/src/vs/base/parts/ipc/electron-main/ipc.mp.ts index 3384832669b..df6422ba227 100644 --- a/src/vs/base/parts/ipc/electron-main/ipc.mp.ts +++ b/src/vs/base/parts/ipc/electron-main/ipc.mp.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BrowserWindow, ipcMain, IpcMainEvent, MessagePortMain } from 'electron'; +import { BrowserWindow, IpcMainEvent, MessagePortMain } from 'electron'; +import { validatedIpcMain } from 'vs/base/parts/ipc/electron-main/ipcMain'; import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import { generateUuid } from 'vs/base/common/uuid'; @@ -50,7 +51,7 @@ export async function connect(window: BrowserWindow): Promise<MessagePortMain> { // Wait until the window has returned the `MessagePort` // We need to filter by the `nonce` to ensure we listen // to the right response. - const onMessageChannelResult = Event.fromNodeEventEmitter<{ nonce: string; port: MessagePortMain }>(ipcMain, 'vscode:createMessageChannelResult', (e: IpcMainEvent, nonce: string) => ({ nonce, port: e.ports[0] })); + const onMessageChannelResult = Event.fromNodeEventEmitter<{ nonce: string; port: MessagePortMain }>(validatedIpcMain, 'vscode:createMessageChannelResult', (e: IpcMainEvent, nonce: string) => ({ nonce, port: e.ports[0] })); const { port } = await Event.toPromise(Event.once(Event.filter(onMessageChannelResult, e => e.nonce === nonce))); return port; diff --git a/src/vs/base/parts/ipc/electron-main/ipcMain.ts b/src/vs/base/parts/ipc/electron-main/ipcMain.ts new file mode 100644 index 00000000000..95e8e4e1c32 --- /dev/null +++ b/src/vs/base/parts/ipc/electron-main/ipcMain.ts @@ -0,0 +1,147 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ipcMain as unsafeIpcMain, IpcMainEvent, IpcMainInvokeEvent } from 'electron'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { Event } from 'vs/base/common/event'; + +type ipcMainListener = (event: IpcMainEvent, ...args: any[]) => void; + +class ValidatedIpcMain implements Event.NodeEventEmitter { + + // We need to keep a map of original listener to the wrapped variant in order + // to properly implement `removeListener`. We use a `WeakMap` because we do + // not want to prevent the `key` of the map to get garbage collected. + private readonly mapListenerToWrapper = new WeakMap<ipcMainListener, ipcMainListener>(); + + /** + * Listens to `channel`, when a new message arrives `listener` would be called with + * `listener(event, args...)`. + */ + on(channel: string, listener: ipcMainListener): this { + + // Remember the wrapped listener so that later we can + // properly implement `removeListener`. + const wrappedListener = (event: IpcMainEvent, ...args: any[]) => { + if (this.validateEvent(channel, event)) { + listener(event, ...args); + } + }; + + this.mapListenerToWrapper.set(listener, wrappedListener); + + unsafeIpcMain.on(channel, wrappedListener); + + return this; + } + + /** + * Adds a one time `listener` function for the event. This `listener` is invoked + * only the next time a message is sent to `channel`, after which it is removed. + */ + once(channel: string, listener: ipcMainListener): this { + unsafeIpcMain.once(channel, (event: IpcMainEvent, ...args: any[]) => { + if (this.validateEvent(channel, event)) { + listener(event, ...args); + } + }); + + return this; + } + + /** + * Adds a handler for an `invoke`able IPC. This handler will be called whenever a + * renderer calls `ipcRenderer.invoke(channel, ...args)`. + * + * If `listener` returns a Promise, the eventual result of the promise will be + * returned as a reply to the remote caller. Otherwise, the return value of the + * listener will be used as the value of the reply. + * + * The `event` that is passed as the first argument to the handler is the same as + * that passed to a regular event listener. It includes information about which + * WebContents is the source of the invoke request. + * + * Errors thrown through `handle` in the main process are not transparent as they + * are serialized and only the `message` property from the original error is + * provided to the renderer process. Please refer to #24427 for details. + */ + handle(channel: string, listener: (event: IpcMainInvokeEvent, ...args: any[]) => Promise<unknown>): this { + unsafeIpcMain.handle(channel, (event: IpcMainInvokeEvent, ...args: any[]) => { + if (this.validateEvent(channel, event)) { + return listener(event, ...args); + } + + return Promise.reject(`Invalid channel '${channel}' or sender for ipcMain.handle() usage.`); + }); + + return this; + } + + /** + * Removes any handler for `channel`, if present. + */ + removeHandler(channel: string): this { + unsafeIpcMain.removeHandler(channel); + + return this; + } + + /** + * Removes the specified `listener` from the listener array for the specified + * `channel`. + */ + removeListener(channel: string, listener: ipcMainListener): this { + const wrappedListener = this.mapListenerToWrapper.get(listener); + if (wrappedListener) { + unsafeIpcMain.removeListener(channel, wrappedListener); + this.mapListenerToWrapper.delete(listener); + } + + return this; + } + + private validateEvent(channel: string, event: IpcMainEvent | IpcMainInvokeEvent): boolean { + if (!channel || !channel.startsWith('vscode:')) { + onUnexpectedError(`Refused to handle ipcMain event for channel '${channel}' because the channel is unknown.`); + return false; // unexpected channel + } + + const sender = event.senderFrame; + + const url = sender.url; + if (!url) { + return true; // TODO@electron this only seems to happen from playwright runs (https://github.com/microsoft/vscode/issues/147301) + } + + let host = 'unknown'; + try { + host = new URL(url).host; + } catch (error) { + onUnexpectedError(`Refused to handle ipcMain event for channel '${channel}' because of a malformed URL '${url}'.`); + return false; // unexpected URL + } + + if (host !== 'vscode-app') { + onUnexpectedError(`Refused to handle ipcMain event for channel '${channel}' because of a bad origin of '${host}'.`); + return false; // unexpected sender + } + + if (sender.parent !== null) { + onUnexpectedError(`Refused to handle ipcMain event for channel '${channel}' because sender of origin '${host}' is not a main frame.`); + return false; // unexpected frame + } + + return true; + } +} + +/** + * A drop-in replacement of `ipcMain` that validates the sender of a message + * according to https://github.com/electron/electron/blob/main/docs/tutorial/security.md + * + * @deprecated direct use of Electron IPC is not encouraged. We have utilities in place + * to create services on top of IPC, see `ProxyChannel` for more information. + */ +export const validatedIpcMain = new ValidatedIpcMain(); diff --git a/src/vs/base/parts/ipc/node/ipc.cp.ts b/src/vs/base/parts/ipc/node/ipc.cp.ts index 127d0082d11..bc8c91530aa 100644 --- a/src/vs/base/parts/ipc/node/ipc.cp.ts +++ b/src/vs/base/parts/ipc/node/ipc.cp.ts @@ -12,7 +12,8 @@ import * as errors from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { deepClone } from 'vs/base/common/objects'; -import { createQueuedSender, removeDangerousEnvVariables } from 'vs/base/node/processes'; +import { createQueuedSender } from 'vs/base/node/processes'; +import { removeDangerousEnvVariables } from 'vs/base/common/processes'; import { ChannelClient as IPCClient, ChannelServer as IPCServer, IChannel, IChannelClient } from 'vs/base/parts/ipc/common/ipc'; /** diff --git a/src/vs/base/parts/quickinput/browser/quickInput.ts b/src/vs/base/parts/quickinput/browser/quickInput.ts index ee3543a2203..0dca8a2bd38 100644 --- a/src/vs/base/parts/quickinput/browser/quickInput.ts +++ b/src/vs/base/parts/quickinput/browser/quickInput.ts @@ -1249,7 +1249,6 @@ export class QuickInputController extends Disposable { const inputBox = this._register(new QuickInputBox(filterContainer)); inputBox.setAttribute('aria-describedby', `${this.idPrefix}message`); - inputBox.setAttribute('aria-live', 'polite'); const visibleCountContainer = dom.append(filterContainer, $('.quick-input-visible-count')); visibleCountContainer.setAttribute('aria-live', 'polite'); @@ -1478,6 +1477,7 @@ export class QuickInputController extends Disposable { input.matchOnLabel = (options.matchOnLabel === undefined) || options.matchOnLabel; // default to true input.autoFocusOnList = (options.autoFocusOnList === undefined) || options.autoFocusOnList; // default to true input.quickNavigate = options.quickNavigate; + input.hideInput = !!options.hideInput; input.contextKey = options.contextKey; input.busy = true; Promise.all([picks, options.activeItem]) diff --git a/src/vs/base/parts/quickinput/common/quickInput.ts b/src/vs/base/parts/quickinput/common/quickInput.ts index 23e42343991..bf9979e00b1 100644 --- a/src/vs/base/parts/quickinput/common/quickInput.ts +++ b/src/vs/base/parts/quickinput/common/quickInput.ts @@ -105,6 +105,13 @@ export interface IPickOptions<T extends IQuickPickItem> { quickNavigate?: IQuickNavigateConfiguration; /** + * Hides the input box from the picker UI. This is typically used + * in combination with quick-navigation where no search UI should + * be presented. + */ + hideInput?: boolean; + + /** * a context key to set when this picker is active */ contextKey?: string; diff --git a/src/vs/base/parts/quickinput/test/browser/quickinput.test.ts b/src/vs/base/parts/quickinput/test/browser/quickinput.test.ts index 05d5511424c..ea365793c29 100644 --- a/src/vs/base/parts/quickinput/test/browser/quickinput.test.ts +++ b/src/vs/base/parts/quickinput/test/browser/quickinput.test.ts @@ -8,6 +8,7 @@ import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/lis import { IListOptions, List } from 'vs/base/browser/ui/list/listWidget'; import { QuickInputController } from 'vs/base/parts/quickinput/browser/quickInput'; import { IQuickPick, IQuickPickItem } from 'vs/base/parts/quickinput/common/quickInput'; +import { flakySuite } from 'vs/base/test/common/testUtils'; // Simple promisify of setTimeout function wait(delayMS: number) { @@ -16,7 +17,7 @@ function wait(delayMS: number) { }); } -suite('QuickInput', () => { +flakySuite('QuickInput', () => { // https://github.com/microsoft/vscode/issues/147543 let fixture: HTMLElement, controller: QuickInputController, quickpick: IQuickPick<IQuickPickItem>; function getScrollTop(): number { diff --git a/src/vs/base/parts/sandbox/electron-browser/preload.js b/src/vs/base/parts/sandbox/electron-browser/preload.js index d8cc4c63220..3e25c4097f1 100644 --- a/src/vs/base/parts/sandbox/electron-browser/preload.js +++ b/src/vs/base/parts/sandbox/electron-browser/preload.js @@ -24,18 +24,6 @@ } /** - * @param {string} type - * @returns {type is 'uncaughtException'} - */ - function validateProcessEventType(type) { - if (type !== 'uncaughtException') { - throw new Error(`Unsupported process event '${type}'`); - } - - return true; - } - - /** * @param {string} key the name of the process argument to parse * @returns {string | undefined} */ @@ -264,6 +252,7 @@ get platform() { return process.platform; }, get arch() { return process.arch; }, get env() { return { ...process.env }; }, + get pid() { return process.pid; }, get versions() { return process.versions; }, get type() { return 'renderer'; }, get execPath() { return process.execPath; }, @@ -293,15 +282,11 @@ /** * @param {string} type * @param {Function} callback - * @returns {ISandboxNodeProcess} + * @returns {void} */ on(type, callback) { - if (validateProcessEventType(type)) { - // @ts-ignore - process.on(type, callback); - - return this; - } + // @ts-ignore + process.on(type, callback); } }, diff --git a/src/vs/base/parts/sandbox/electron-sandbox/globals.ts b/src/vs/base/parts/sandbox/electron-sandbox/globals.ts index 698d886ce43..7d9b29a6e3a 100644 --- a/src/vs/base/parts/sandbox/electron-sandbox/globals.ts +++ b/src/vs/base/parts/sandbox/electron-sandbox/globals.ts @@ -35,6 +35,11 @@ export interface ISandboxNodeProcess extends INodeProcess { readonly sandboxed: boolean; /** + * The `process.pid` property returns the PID of the process. + */ + readonly pid: number; + + /** * A list of versions for the current node.js/electron configuration. */ readonly versions: { [key: string]: string | undefined }; diff --git a/src/vs/base/test/common/filters.perf.test.ts b/src/vs/base/test/common/filters.perf.test.ts index a8b33d7fc8b..8ac5a3c89d9 100644 --- a/src/vs/base/test/common/filters.perf.test.ts +++ b/src/vs/base/test/common/filters.perf.test.ts @@ -32,7 +32,7 @@ perfSuite('Performance - fuzzyMatch', function () { const patternLow = pattern.toLowerCase(); for (const item of data) { count += 1; - match(pattern, patternLow, 0, item, item.toLowerCase(), 0, false); + match(pattern, patternLow, 0, item, item.toLowerCase(), 0); } } } diff --git a/src/vs/base/test/common/filters.test.ts b/src/vs/base/test/common/filters.test.ts index c69d027fc96..5fcd4f9f097 100644 --- a/src/vs/base/test/common/filters.test.ts +++ b/src/vs/base/test/common/filters.test.ts @@ -223,7 +223,7 @@ suite('Filters', () => { }); function assertMatches(pattern: string, word: string, decoratedWord: string | undefined, filter: FuzzyScorer, opts: { patternPos?: number; wordPos?: number; firstMatchCanBeWeak?: boolean } = {}) { - let r = filter(pattern, pattern.toLowerCase(), opts.patternPos || 0, word, word.toLowerCase(), opts.wordPos || 0, opts.firstMatchCanBeWeak || false); + let r = filter(pattern, pattern.toLowerCase(), opts.patternPos || 0, word, word.toLowerCase(), opts.wordPos || 0, { firstMatchCanBeWeak: opts.firstMatchCanBeWeak ?? false, boostFullMatch: true }); assert.ok(!decoratedWord === !r); if (r) { let matches = createMatches(r); @@ -405,7 +405,7 @@ suite('Filters', () => { test('Cannot set property \'1\' of undefined, #26511', function () { let word = new Array<void>(123).join('a'); let pattern = new Array<void>(120).join('a'); - fuzzyScore(pattern, pattern.toLowerCase(), 0, word, word.toLowerCase(), 0, false); + fuzzyScore(pattern, pattern.toLowerCase(), 0, word, word.toLowerCase(), 0); assert.ok(true); // must not explode }); @@ -435,7 +435,7 @@ suite('Filters', () => { let topIdx = 0; for (let i = 0; i < words.length; i++) { const word = words[i]; - const m = filter(pattern, pattern.toLowerCase(), 0, word, word.toLowerCase(), 0, false); + const m = filter(pattern, pattern.toLowerCase(), 0, word, word.toLowerCase(), 0); if (m) { const [score] = m; if (score > topScore) { @@ -538,7 +538,7 @@ suite('Filters', () => { }); test('"Go to Symbol" with the exact method name doesn\'t work as expected #84787', function () { - const match = fuzzyScore(':get', ':get', 1, 'get', 'get', 0, true); + const match = fuzzyScore(':get', ':get', 1, 'get', 'get', 0, { firstMatchCanBeWeak: true, boostFullMatch: true }); assert.ok(Boolean(match)); }); @@ -557,4 +557,28 @@ suite('Filters', () => { assertMatches('.lo', 'log', '^l^og', anyScore); assertMatches('.', 'log', 'log', anyScore); }); + + test('configurable full match boost', function () { + let prefix = 'create'; + let a = 'createModelServices'; + let b = 'create'; + + let aBoost = fuzzyScore(prefix, prefix, 0, a, a.toLowerCase(), 0, { boostFullMatch: true, firstMatchCanBeWeak: true }); + let bBoost = fuzzyScore(prefix, prefix, 0, b, b.toLowerCase(), 0, { boostFullMatch: true, firstMatchCanBeWeak: true }); + assert.ok(aBoost); + assert.ok(bBoost); + assert.ok(aBoost[0] < bBoost[0]); + + let aScore = fuzzyScore(prefix, prefix, 0, a, a.toLowerCase(), 0, { boostFullMatch: false, firstMatchCanBeWeak: true }); + let bScore = fuzzyScore(prefix, prefix, 0, b, b.toLowerCase(), 0, { boostFullMatch: false, firstMatchCanBeWeak: true }); + assert.ok(aScore); + assert.ok(bScore); + assert.ok(aScore[0] === bScore[0]); + }); + + test('Unexpected suggest highlighting ignores whole word match in favor of matching first letter#147423', function () { + + assertMatches('i', 'machine/{id}', 'machine/{^id}', fuzzyScore); + assertMatches('ok', 'obobobf{ok}/user', '^obobobf{o^k}/user', fuzzyScore); + }); }); diff --git a/src/vs/base/test/common/glob.test.ts b/src/vs/base/test/common/glob.test.ts index ba31ed0099c..7963c4c4139 100644 --- a/src/vs/base/test/common/glob.test.ts +++ b/src/vs/base/test/common/glob.test.ts @@ -1066,6 +1066,18 @@ suite('Glob', () => { } }); + test('relative pattern - single star alone', function () { + if (isWindows) { + let p: glob.IRelativePattern = { base: 'C:\\DNXConsoleApp\\foo\\something\\Program.cs', pattern: '*' }; + assertGlobMatch(p, 'C:\\DNXConsoleApp\\foo\\something\\Program.cs'); + assertNoGlobMatch(p, 'C:\\DNXConsoleApp\\foo\\Program.cs'); + } else { + let p: glob.IRelativePattern = { base: '/DNXConsoleApp/foo/something/Program.cs', pattern: '*' }; + assertGlobMatch(p, '/DNXConsoleApp/foo/something/Program.cs'); + assertNoGlobMatch(p, '/DNXConsoleApp/foo/Program.cs'); + } + }); + test('relative pattern - ignores case on macOS/Windows', function () { if (isWindows) { let p: glob.IRelativePattern = { base: 'C:\\DNXConsoleApp\\foo', pattern: 'something/*.cs' }; @@ -1099,4 +1111,33 @@ suite('Glob', () => { let p = 'scheme:/**/*.md'; assertGlobMatch(p, URI.file('super/duper/long/some/file.md').with({ scheme: 'scheme' }).toString()); }); + + test('expression fails when siblings use promises (https://github.com/microsoft/vscode/issues/146294)', async function () { + let siblings = ['test.html', 'test.txt', 'test.ts']; + let hasSibling = (name: string) => Promise.resolve(siblings.indexOf(name) !== -1); + + // { "**/*.js": { "when": "$(basename).ts" } } + let expression: glob.IExpression = { + '**/test.js': { when: '$(basename).js' }, + '**/*.js': { when: '$(basename).ts' } + }; + + const parsedExpression = glob.parse(expression); + + assert.strictEqual('**/*.js', await parsedExpression('test.js', undefined, hasSibling)); + }); + + test('patternsEquals', () => { + assert.ok(glob.patternsEquals(['a'], ['a'])); + assert.ok(!glob.patternsEquals(['a'], ['b'])); + + assert.ok(glob.patternsEquals(['a', 'b', 'c'], ['a', 'b', 'c'])); + assert.ok(!glob.patternsEquals(['1', '2'], ['1', '3'])); + + assert.ok(glob.patternsEquals([{ base: 'a', pattern: '*' }, 'b', 'c'], [{ base: 'a', pattern: '*' }, 'b', 'c'])); + + assert.ok(glob.patternsEquals(undefined, undefined)); + assert.ok(!glob.patternsEquals(undefined, ['b'])); + assert.ok(!glob.patternsEquals(['a'], undefined)); + }); }); diff --git a/src/vs/base/test/common/labels.test.ts b/src/vs/base/test/common/labels.test.ts index b01853ba03c..49cc1699699 100644 --- a/src/vs/base/test/common/labels.test.ts +++ b/src/vs/base/test/common/labels.test.ts @@ -5,7 +5,8 @@ import * as assert from 'assert'; import * as labels from 'vs/base/common/labels'; -import { isMacintosh, isWindows } from 'vs/base/common/platform'; +import { isMacintosh, isWindows, OperatingSystem } from 'vs/base/common/platform'; +import { URI } from 'vs/base/common/uri'; suite('Labels', () => { (!isWindows ? test.skip : test)('shorten - windows', () => { @@ -14,6 +15,8 @@ suite('Labels', () => { assert.deepStrictEqual(labels.shorten(['a']), ['a']); assert.deepStrictEqual(labels.shorten(['a', 'b']), ['a', 'b']); assert.deepStrictEqual(labels.shorten(['a', 'b', 'c']), ['a', 'b', 'c']); + assert.deepStrictEqual(labels.shorten(['\\\\x\\a', '\\\\x\\a']), ['\\\\x\\a', '\\\\x\\a']); + assert.deepStrictEqual(labels.shorten(['C:\\a', 'C:\\b']), ['C:\\a', 'C:\\b']); // completely different paths assert.deepStrictEqual(labels.shorten(['a\\b', 'c\\d', 'e\\f']), ['…\\b', '…\\d', '…\\f']); @@ -64,6 +67,8 @@ suite('Labels', () => { // nothing to shorten assert.deepStrictEqual(labels.shorten(['a']), ['a']); assert.deepStrictEqual(labels.shorten(['a', 'b']), ['a', 'b']); + assert.deepStrictEqual(labels.shorten(['/a', '/b']), ['/a', '/b']); + assert.deepStrictEqual(labels.shorten(['~/a/b/c', '~/a/b/c']), ['~/a/b/c', '~/a/b/c']); assert.deepStrictEqual(labels.shorten(['a', 'b', 'c']), ['a', 'b', 'c']); // completely different paths @@ -162,4 +167,78 @@ suite('Labels', () => { assert.strictEqual(labels.mnemonicButtonLabel('Do &¬ Save & Continue'), 'Do _not Save & Continue'); } }); + + test('getPathLabel', () => { + const winFileUri = URI.file('c:/some/folder/file.txt'); + const nixFileUri = URI.file('/some/folder/file.txt'); + const uncFileUri = URI.file('c:/some/folder/file.txt').with({ authority: 'auth' }); + const remoteFileUri = URI.file('/some/folder/file.txt').with({ scheme: 'vscode-test', authority: 'auth' }); + + // Basics + + assert.strictEqual(labels.getPathLabel(winFileUri, { os: OperatingSystem.Windows }), 'C:\\some\\folder\\file.txt'); + assert.strictEqual(labels.getPathLabel(winFileUri, { os: OperatingSystem.Macintosh }), 'c:/some/folder/file.txt'); + assert.strictEqual(labels.getPathLabel(winFileUri, { os: OperatingSystem.Linux }), 'c:/some/folder/file.txt'); + + assert.strictEqual(labels.getPathLabel(nixFileUri, { os: OperatingSystem.Windows }), '\\some\\folder\\file.txt'); + assert.strictEqual(labels.getPathLabel(nixFileUri, { os: OperatingSystem.Macintosh }), '/some/folder/file.txt'); + assert.strictEqual(labels.getPathLabel(nixFileUri, { os: OperatingSystem.Linux }), '/some/folder/file.txt'); + + assert.strictEqual(labels.getPathLabel(uncFileUri, { os: OperatingSystem.Windows }), '\\\\auth\\c:\\some\\folder\\file.txt'); + assert.strictEqual(labels.getPathLabel(uncFileUri, { os: OperatingSystem.Macintosh }), '/auth/c:/some/folder/file.txt'); + assert.strictEqual(labels.getPathLabel(uncFileUri, { os: OperatingSystem.Linux }), '/auth/c:/some/folder/file.txt'); + + assert.strictEqual(labels.getPathLabel(remoteFileUri, { os: OperatingSystem.Windows }), '\\some\\folder\\file.txt'); + assert.strictEqual(labels.getPathLabel(remoteFileUri, { os: OperatingSystem.Macintosh }), '/some/folder/file.txt'); + assert.strictEqual(labels.getPathLabel(remoteFileUri, { os: OperatingSystem.Linux }), '/some/folder/file.txt'); + + // Tildify + + const nixUserHome = URI.file('/some'); + const remoteUserHome = URI.file('/some').with({ scheme: 'vscode-test', authority: 'auth' }); + + assert.strictEqual(labels.getPathLabel(nixFileUri, { os: OperatingSystem.Windows, tildify: { userHome: nixUserHome } }), '\\some\\folder\\file.txt'); + assert.strictEqual(labels.getPathLabel(nixFileUri, { os: OperatingSystem.Macintosh, tildify: { userHome: nixUserHome } }), '~/folder/file.txt'); + assert.strictEqual(labels.getPathLabel(nixFileUri, { os: OperatingSystem.Linux, tildify: { userHome: nixUserHome } }), '~/folder/file.txt'); + + assert.strictEqual(labels.getPathLabel(nixFileUri, { os: OperatingSystem.Windows, tildify: { userHome: remoteUserHome } }), '\\some\\folder\\file.txt'); + assert.strictEqual(labels.getPathLabel(nixFileUri, { os: OperatingSystem.Macintosh, tildify: { userHome: remoteUserHome } }), '~/folder/file.txt'); + assert.strictEqual(labels.getPathLabel(nixFileUri, { os: OperatingSystem.Linux, tildify: { userHome: remoteUserHome } }), '~/folder/file.txt'); + + const nixUntitledUri = URI.file('/some/folder/file.txt').with({ scheme: 'untitled' }); + + assert.strictEqual(labels.getPathLabel(nixUntitledUri, { os: OperatingSystem.Windows, tildify: { userHome: nixUserHome } }), '\\some\\folder\\file.txt'); + assert.strictEqual(labels.getPathLabel(nixUntitledUri, { os: OperatingSystem.Macintosh, tildify: { userHome: nixUserHome } }), '~/folder/file.txt'); + assert.strictEqual(labels.getPathLabel(nixUntitledUri, { os: OperatingSystem.Linux, tildify: { userHome: nixUserHome } }), '~/folder/file.txt'); + + assert.strictEqual(labels.getPathLabel(nixUntitledUri, { os: OperatingSystem.Windows, tildify: { userHome: remoteUserHome } }), '\\some\\folder\\file.txt'); + assert.strictEqual(labels.getPathLabel(nixUntitledUri, { os: OperatingSystem.Macintosh, tildify: { userHome: remoteUserHome } }), '~/folder/file.txt'); + assert.strictEqual(labels.getPathLabel(nixUntitledUri, { os: OperatingSystem.Linux, tildify: { userHome: remoteUserHome } }), '~/folder/file.txt'); + + // Relative + + const winFolder = URI.file('c:/some'); + const winRelativePathProvider: labels.IRelativePathProvider = { + getWorkspace() { return { folders: [{ uri: winFolder }] }; }, + getWorkspaceFolder(resource) { return { uri: winFolder }; } + }; + + assert.strictEqual(labels.getPathLabel(winFileUri, { os: OperatingSystem.Windows, relative: winRelativePathProvider }), 'folder\\file.txt'); + assert.strictEqual(labels.getPathLabel(winFileUri, { os: OperatingSystem.Macintosh, relative: winRelativePathProvider }), 'folder/file.txt'); + assert.strictEqual(labels.getPathLabel(winFileUri, { os: OperatingSystem.Linux, relative: winRelativePathProvider }), 'folder/file.txt'); + + const nixFolder = URI.file('/some'); + const nixRelativePathProvider: labels.IRelativePathProvider = { + getWorkspace() { return { folders: [{ uri: nixFolder }] }; }, + getWorkspaceFolder(resource) { return { uri: nixFolder }; } + }; + + assert.strictEqual(labels.getPathLabel(nixFileUri, { os: OperatingSystem.Windows, relative: nixRelativePathProvider }), 'folder\\file.txt'); + assert.strictEqual(labels.getPathLabel(nixFileUri, { os: OperatingSystem.Macintosh, relative: nixRelativePathProvider }), 'folder/file.txt'); + assert.strictEqual(labels.getPathLabel(nixFileUri, { os: OperatingSystem.Linux, relative: nixRelativePathProvider }), 'folder/file.txt'); + + assert.strictEqual(labels.getPathLabel(nixUntitledUri, { os: OperatingSystem.Windows, relative: nixRelativePathProvider }), 'folder\\file.txt'); + assert.strictEqual(labels.getPathLabel(nixUntitledUri, { os: OperatingSystem.Macintosh, relative: nixRelativePathProvider }), 'folder/file.txt'); + assert.strictEqual(labels.getPathLabel(nixUntitledUri, { os: OperatingSystem.Linux, relative: nixRelativePathProvider }), 'folder/file.txt'); + }); }); diff --git a/src/vs/base/test/common/map.test.ts b/src/vs/base/test/common/map.test.ts index c681cf92396..1e2a792cab8 100644 --- a/src/vs/base/test/common/map.test.ts +++ b/src/vs/base/test/common/map.test.ts @@ -371,7 +371,7 @@ suite('Map', () => { }); test('URIIterator', function () { - const iter = new UriIterator(() => false); + const iter = new UriIterator(() => false, () => false); iter.reset(URI.parse('file:///usr/bin/file.txt')); assert.strictEqual(iter.value(), 'file'); @@ -429,6 +429,58 @@ suite('Map', () => { assert.strictEqual(iter.hasNext(), false); }); + test('URIIterator - ignore query/fragment', function () { + const iter = new UriIterator(() => false, () => true); + iter.reset(URI.parse('file:///usr/bin/file.txt')); + + assert.strictEqual(iter.value(), 'file'); + // assert.strictEqual(iter.cmp('FILE'), 0); + assert.strictEqual(iter.cmp('file'), 0); + assert.strictEqual(iter.hasNext(), true); + iter.next(); + + assert.strictEqual(iter.value(), 'usr'); + assert.strictEqual(iter.hasNext(), true); + iter.next(); + + assert.strictEqual(iter.value(), 'bin'); + assert.strictEqual(iter.hasNext(), true); + iter.next(); + + assert.strictEqual(iter.value(), 'file.txt'); + assert.strictEqual(iter.hasNext(), false); + + + iter.reset(URI.parse('file://share/usr/bin/file.txt?foo')); + + // scheme + assert.strictEqual(iter.value(), 'file'); + // assert.strictEqual(iter.cmp('FILE'), 0); + assert.strictEqual(iter.cmp('file'), 0); + assert.strictEqual(iter.hasNext(), true); + iter.next(); + + // authority + assert.strictEqual(iter.value(), 'share'); + assert.strictEqual(iter.cmp('SHARe'), 0); + assert.strictEqual(iter.hasNext(), true); + iter.next(); + + // path + assert.strictEqual(iter.value(), 'usr'); + assert.strictEqual(iter.hasNext(), true); + iter.next(); + + // path + assert.strictEqual(iter.value(), 'bin'); + assert.strictEqual(iter.hasNext(), true); + iter.next(); + + // path + assert.strictEqual(iter.value(), 'file.txt'); + assert.strictEqual(iter.hasNext(), false); + }); + function assertTstDfs<E>(trie: TernarySearchTree<string, E>, ...elements: [string, E][]) { assert.ok(trie._isBalanced(), 'TST is not balanced'); @@ -901,7 +953,7 @@ suite('Map', () => { }); test('TernarySearchTree (URI) - basics', function () { - let trie = new TernarySearchTree<URI, number>(new UriIterator(() => false)); + let trie = new TernarySearchTree<URI, number>(new UriIterator(() => false, () => false)); trie.set(URI.file('/user/foo/bar'), 1); trie.set(URI.file('/user/foo'), 2); @@ -919,9 +971,20 @@ suite('Map', () => { assert.strictEqual(trie.findSubstr(URI.file('/user/foo/bar/far/boo')), 1); }); + test('TernarySearchTree (URI) - query parameters', function () { + let trie = new TernarySearchTree<URI, number>(new UriIterator(() => false, () => true)); + const root = URI.parse('memfs:/?param=1'); + trie.set(root, 1); + + assert.strictEqual(trie.get(URI.parse('memfs:/?param=1')), 1); + + assert.strictEqual(trie.findSubstr(URI.parse('memfs:/?param=1')), 1); + assert.strictEqual(trie.findSubstr(URI.parse('memfs:/aaa?param=1')), 1); + }); + test('TernarySearchTree (URI) - lookup', function () { - const map = new TernarySearchTree<URI, number>(new UriIterator(() => false)); + const map = new TernarySearchTree<URI, number>(new UriIterator(() => false, () => false)); map.set(URI.parse('http://foo.bar/user/foo/bar'), 1); map.set(URI.parse('http://foo.bar/user/foo?query'), 2); map.set(URI.parse('http://foo.bar/user/foo?QUERY'), 3); @@ -938,7 +1001,7 @@ suite('Map', () => { test('TernarySearchTree (URI) - lookup, casing', function () { - const map = new TernarySearchTree<URI, number>(new UriIterator(uri => /^https?$/.test(uri.scheme))); + const map = new TernarySearchTree<URI, number>(new UriIterator(uri => /^https?$/.test(uri.scheme), () => false)); map.set(URI.parse('http://foo.bar/user/foo/bar'), 1); assert.strictEqual(map.get(URI.parse('http://foo.bar/USER/foo/bar')), 1); @@ -948,7 +1011,7 @@ suite('Map', () => { test('TernarySearchTree (URI) - superstr', function () { - const map = new TernarySearchTree<URI, number>(new UriIterator(() => false)); + const map = new TernarySearchTree<URI, number>(new UriIterator(() => false, () => false)); map.set(URI.file('/user/foo/bar'), 1); map.set(URI.file('/user/foo'), 2); map.set(URI.file('/user/foo/flip/flop'), 3); diff --git a/src/vs/base/test/common/strings.test.ts b/src/vs/base/test/common/strings.test.ts index 8d27e715931..6744ea9a371 100644 --- a/src/vs/base/test/common/strings.test.ts +++ b/src/vs/base/test/common/strings.test.ts @@ -386,4 +386,13 @@ suite('Strings', () => { assert.strictEqual('hello world', strings.truncate('hello world', 100)); assert.strictEqual('hello…', strings.truncate('hello world', 5)); }); + + test('replaceAsync', async () => { + let i = 0; + assert.strictEqual(await strings.replaceAsync('abcabcabcabc', /b(.)/g, async (match, after) => { + assert.strictEqual(match, 'bc'); + assert.strictEqual(after, 'c'); + return `${i++}${after}`; + }), 'a0ca1ca2ca3c'); + }); }); diff --git a/src/vs/code/browser/workbench/workbench-dev.html b/src/vs/code/browser/workbench/workbench-dev.html index 86df5ff3666..1c36a7a0c72 100644 --- a/src/vs/code/browser/workbench/workbench-dev.html +++ b/src/vs/code/browser/workbench/workbench-dev.html @@ -11,7 +11,7 @@ <meta name="mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-title" content="Code"> - <link rel="apple-touch-icon" href="/code-192.png" /> + <link rel="apple-touch-icon" href="{{WORKBENCH_WEB_BASE_URL}}/resources/server/code-192.png" /> <!-- Disable pinch zooming --> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no"> @@ -26,22 +26,23 @@ <meta id="vscode-workbench-builtin-extensions" data-settings="{{WORKBENCH_BUILTIN_EXTENSIONS}}"> <!-- Workbench Icon/Manifest/CSS --> - <link rel="icon" href="/favicon.ico" type="image/x-icon" /> - <link rel="manifest" href="/manifest.json" crossorigin="use-credentials" /> + <link rel="icon" href="{{WORKBENCH_WEB_BASE_URL}}/resources/server/favicon.ico" type="image/x-icon" /> + <link rel="manifest" href="{{WORKBENCH_WEB_BASE_URL}}/resources/server/manifest.json" crossorigin="use-credentials" /> </head> <body aria-label=""> </body> <!-- Startup (do not modify order of script tags!) --> - <script src="./static/out/vs/loader.js"></script> - <script src="./static/out/vs/webPackagePaths.js"></script> + <script src="{{WORKBENCH_WEB_BASE_URL}}/out/vs/loader.js"></script> + <script src="{{WORKBENCH_WEB_BASE_URL}}/out/vs/webPackagePaths.js"></script> <script> + const baseUrl = new URL('{{WORKBENCH_WEB_BASE_URL}}', window.location.origin).toString(); Object.keys(self.webPackagePaths).map(function (key, index) { - self.webPackagePaths[key] = `${window.location.origin}/static/remote/web/node_modules/${key}/${self.webPackagePaths[key]}`; + self.webPackagePaths[key] = `${baseUrl}/remote/web/node_modules/${key}/${self.webPackagePaths[key]}`; }); require.config({ - baseUrl: `${window.location.origin}/static/out`, + baseUrl: `${baseUrl}/out`, recordStats: true, trustedTypesPolicy: window.trustedTypes?.createPolicy('amdLoader', { createScriptURL(value) { diff --git a/src/vs/code/browser/workbench/workbench.html b/src/vs/code/browser/workbench/workbench.html index 02288223a8b..2e4eddb19b4 100644 --- a/src/vs/code/browser/workbench/workbench.html +++ b/src/vs/code/browser/workbench/workbench.html @@ -11,7 +11,7 @@ <meta name="mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-title" content="Code"> - <link rel="apple-touch-icon" href="/code-192.png" /> + <link rel="apple-touch-icon" href="{{WORKBENCH_WEB_BASE_URL}}/resources/server/code-192.png" /> <!-- Disable pinch zooming --> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no"> @@ -23,9 +23,9 @@ <meta id="vscode-workbench-auth-session" data-settings="{{WORKBENCH_AUTH_SESSION}}"> <!-- Workbench Icon/Manifest/CSS --> - <link rel="icon" href="/favicon.ico" type="image/x-icon" /> - <link rel="manifest" href="/manifest.json" crossorigin="use-credentials" /> - <link data-name="vs/workbench/workbench.web.main" rel="stylesheet" href="./static/out/vs/workbench/workbench.web.main.css"> + <link rel="icon" href="{{WORKBENCH_WEB_BASE_URL}}/resources/server/favicon.ico" type="image/x-icon" /> + <link rel="manifest" href="{{WORKBENCH_WEB_BASE_URL}}/resources/server/manifest.json" crossorigin="use-credentials" /> + <link data-name="vs/workbench/workbench.web.main" rel="stylesheet" href="{{WORKBENCH_WEB_BASE_URL}}/out/vs/workbench/workbench.web.main.css"> </head> @@ -33,14 +33,15 @@ </body> <!-- Startup (do not modify order of script tags!) --> - <script src="./static/out/vs/loader.js"></script> - <script src="./static/out/vs/webPackagePaths.js"></script> + <script src="{{WORKBENCH_WEB_BASE_URL}}/out/vs/loader.js"></script> + <script src="{{WORKBENCH_WEB_BASE_URL}}/out/vs/webPackagePaths.js"></script> <script> + const baseUrl = new URL('{{WORKBENCH_WEB_BASE_URL}}', window.location.origin).toString(); Object.keys(self.webPackagePaths).map(function (key, index) { - self.webPackagePaths[key] = `${window.location.origin}/static/node_modules/${key}/${self.webPackagePaths[key]}`; + self.webPackagePaths[key] = `${baseUrl}/node_modules/${key}/${self.webPackagePaths[key]}`; }); require.config({ - baseUrl: `${window.location.origin}/static/out`, + baseUrl: `${baseUrl}/out`, recordStats: true, trustedTypesPolicy: window.trustedTypes?.createPolicy('amdLoader', { createScriptURL(value) { @@ -53,7 +54,7 @@ <script> performance.mark('code/willLoadWorkbenchMain'); </script> - <script src="./static/out/vs/workbench/workbench.web.main.nls.js"></script> - <script src="./static/out/vs/workbench/workbench.web.main.js"></script> - <script src="./static/out/vs/code/browser/workbench/workbench.js"></script> + <script src="{{WORKBENCH_WEB_BASE_URL}}/out/vs/workbench/workbench.web.main.nls.js"></script> + <script src="{{WORKBENCH_WEB_BASE_URL}}/out/vs/workbench/workbench.web.main.js"></script> + <script src="{{WORKBENCH_WEB_BASE_URL}}/out/vs/code/browser/workbench/workbench.js"></script> </html> diff --git a/src/vs/code/browser/workbench/workbench.ts b/src/vs/code/browser/workbench/workbench.ts index 612f54c17a1..cad7de3f9ed 100644 --- a/src/vs/code/browser/workbench/workbench.ts +++ b/src/vs/code/browser/workbench/workbench.ts @@ -192,6 +192,10 @@ class LocalStorageURLCallbackProvider extends Disposable implements IURLCallback private checkCallbacksTimeout: unknown | undefined = undefined; private onDidChangeLocalStorageDisposable: IDisposable | undefined; + constructor(private readonly _callbackRoute: string) { + super(); + } + create(options: Partial<UriComponents> = {}): URI { const id = ++LocalStorageURLCallbackProvider.REQUEST_ID; const queryParams: string[] = [`vscode-reqid=${id}`]; @@ -215,7 +219,7 @@ class LocalStorageURLCallbackProvider extends Disposable implements IURLCallback this.startListening(); } - return URI.parse(window.location.href).with({ path: '/callback', query: queryParams.join('&') }); + return URI.parse(window.location.href).with({ path: this._callbackRoute, query: queryParams.join('&') }); } private startListening(): void { @@ -492,7 +496,7 @@ function doCreateUri(path: string, queryValues: Map<string, string>): URI { if (!configElement || !configElementAttribute) { throw new Error('Missing web configuration element'); } - const config: IWorkbenchConstructionOptions & { folderUri?: UriComponents; workspaceUri?: UriComponents } = JSON.parse(configElementAttribute); + const config: IWorkbenchConstructionOptions & { folderUri?: UriComponents; workspaceUri?: UriComponents; callbackRoute: string } = JSON.parse(configElementAttribute); // Create workbench create(document.body, { @@ -501,7 +505,7 @@ function doCreateUri(path: string, queryValues: Map<string, string>): URI { enabled: config.settingsSyncOptions.enabled, } : undefined, workspaceProvider: WorkspaceProvider.create(config), - urlCallbackProvider: new LocalStorageURLCallbackProvider(), + urlCallbackProvider: new LocalStorageURLCallbackProvider(config.callbackRoute), credentialsProvider: config.remoteAuthority ? undefined : new LocalStorageCredentialsProvider() // with a remote, we don't use a local credentials provider }); })(); diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index 0166a2b0e5d..4dd6d42da9b 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -98,6 +98,8 @@ import { FileUserDataProvider } from 'vs/platform/userData/common/fileUserDataPr import { DiskFileSystemProviderClient, LOCAL_FILE_SYSTEM_CHANNEL_NAME } from 'vs/platform/files/common/diskFileSystemProviderClient'; import { InspectProfilingService as V8InspectProfilingService } from 'vs/platform/profiling/node/profilingService'; import { IV8InspectProfilingService } from 'vs/platform/profiling/common/profiling'; +import { IExtensionsScannerService } from 'vs/platform/extensionManagement/common/extensionsScannerService'; +import { ExtensionsScannerService } from 'vs/platform/extensionManagement/node/extensionsScannerService'; class SharedProcessMain extends Disposable { @@ -293,6 +295,7 @@ class SharedProcessMain extends Disposable { services.set(ICustomEndpointTelemetryService, customEndpointTelemetryService); // Extension Management + services.set(IExtensionsScannerService, new SyncDescriptor(ExtensionsScannerService)); services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); // Extension Gallery @@ -330,7 +333,7 @@ class SharedProcessMain extends Disposable { environmentService, logService ); - await ptyHostService.initialize(); + ptyHostService.initialize(); // Terminal services.set(ILocalPtyService, this._register(ptyHostService)); diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 5d6b367dca0..169ba778fe1 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { app, BrowserWindow, contentTracing, dialog, ipcMain, protocol, session, Session, systemPreferences, WebFrameMain } from 'electron'; +import { app, BrowserWindow, contentTracing, dialog, protocol, session, Session, systemPreferences, WebFrameMain } from 'electron'; +import { validatedIpcMain } from 'vs/base/parts/ipc/electron-main/ipcMain'; import { statSync } from 'fs'; import { hostname, release } from 'os'; import { VSBuffer } from 'vs/base/common/buffer'; @@ -16,7 +17,7 @@ import { getPathLabel, mnemonicButtonLabel } from 'vs/base/common/labels'; import { Disposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { isAbsolute, join, posix } from 'vs/base/common/path'; -import { IProcessEnvironment, isLinux, isLinuxSnap, isMacintosh, isWindows } from 'vs/base/common/platform'; +import { IProcessEnvironment, isLinux, isLinuxSnap, isMacintosh, isWindows, OS } from 'vs/base/common/platform'; import { assertType, withNullAsUndefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; @@ -36,7 +37,6 @@ import { ElectronExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/el import { IDiagnosticsService } from 'vs/platform/diagnostics/common/diagnostics'; import { DiagnosticsMainService, IDiagnosticsMainService } from 'vs/platform/diagnostics/electron-main/diagnosticsMainService'; import { DialogMainService, IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService'; -import { serve as serveDriver } from 'vs/platform/driver/electron-main/driver'; import { IEncryptionMainService } from 'vs/platform/encryption/common/encryptionService'; import { EncryptionMainService } from 'vs/platform/encryption/node/encryptionMainService'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; @@ -380,7 +380,7 @@ export class CodeApplication extends Disposable { //#region Bootstrap IPC Handlers - ipcMain.handle('vscode:fetchShellEnv', event => { + validatedIpcMain.handle('vscode:fetchShellEnv', event => { // Prefer to use the args and env from the target window // when resolving the shell env. It is possible that @@ -405,7 +405,7 @@ export class CodeApplication extends Disposable { return this.resolveShellEnvironment(args, env, false); }); - ipcMain.handle('vscode:writeNlsFile', (event, path: unknown, data: unknown) => { + validatedIpcMain.handle('vscode:writeNlsFile', (event, path: unknown, data: unknown) => { const uri = this.validateNlsPath([path]); if (!uri || typeof data !== 'string') { throw new Error('Invalid operation (vscode:writeNlsFile)'); @@ -414,7 +414,7 @@ export class CodeApplication extends Disposable { return this.fileService.writeFile(uri, VSBuffer.fromString(data)); }); - ipcMain.handle('vscode:readNlsFile', async (event, ...paths: unknown[]) => { + validatedIpcMain.handle('vscode:readNlsFile', async (event, ...paths: unknown[]) => { const uri = this.validateNlsPath(paths); if (!uri) { throw new Error('Invalid operation (vscode:readNlsFile)'); @@ -423,10 +423,10 @@ export class CodeApplication extends Disposable { return (await this.fileService.readFile(uri)).value.toString(); }); - ipcMain.on('vscode:toggleDevTools', event => event.sender.toggleDevTools()); - ipcMain.on('vscode:openDevTools', event => event.sender.openDevTools()); + validatedIpcMain.on('vscode:toggleDevTools', event => event.sender.toggleDevTools()); + validatedIpcMain.on('vscode:openDevTools', event => event.sender.openDevTools()); - ipcMain.on('vscode:reloadWindow', event => event.sender.reload()); + validatedIpcMain.on('vscode:reloadWindow', event => event.sender.reload()); //#endregion } @@ -522,14 +522,6 @@ export class CodeApplication extends Disposable { // Services const appInstantiationService = await this.initServices(machineId, sharedProcess, sharedProcessReady); - // Create driver - if (this.environmentMainService.driverHandle) { - const server = await serveDriver(mainProcessElectronServer, this.environmentMainService.driverHandle, appInstantiationService); - - this.logService.info('Driver started at:', this.environmentMainService.driverHandle); - this._register(server); - } - // Setup Auth Handler this._register(appInstantiationService.createInstance(ProxyAuthHandler)); @@ -859,6 +851,19 @@ export class CodeApplication extends Disposable { return true; } + let shouldOpenInNewWindow = false; + + // We should handle the URI in a new window if the URL contains `windowId=_blank` + const params = new URLSearchParams(uri.query); + if (params.get('windowId') === '_blank') { + params.delete('windowId'); + uri = uri.with({ query: params.toString() }); + shouldOpenInNewWindow = true; + } + + // or if no window is open (macOS only) + shouldOpenInNewWindow ||= isMacintosh && windowsMainService.getWindowCount() === 0; + // Check for URIs to open in window const windowOpenableFromProtocolLink = app.getWindowOpenableFromProtocolLink(uri); logService.trace('app#handleURL: windowOpenableFromProtocolLink = ', windowOpenableFromProtocolLink); @@ -867,6 +872,7 @@ export class CodeApplication extends Disposable { context: OpenContext.API, cli: { ...environmentService.args }, urisToOpen: [windowOpenableFromProtocolLink], + forceNewWindow: shouldOpenInNewWindow, gotoLineMode: true // remoteAuthority: will be determined based on windowOpenableFromProtocolLink }); @@ -876,12 +882,11 @@ export class CodeApplication extends Disposable { return true; } - // If we have not yet handled the URI and we have no window opened (macOS only) - // we first open a window and then try to open that URI within that window - if (isMacintosh && windowsMainService.getWindowCount() === 0) { + if (shouldOpenInNewWindow) { const [window] = windowsMainService.open({ context: OpenContext.API, cli: { ...environmentService.args }, + forceNewWindow: true, forceEmpty: true, gotoLineMode: true, remoteAuthority: getRemoteAuthority(uri) @@ -987,7 +992,7 @@ export class CodeApplication extends Disposable { ], defaultId: 0, cancelId: 1, - message: localize('confirmOpenMessage', "An external application wants to open '{0}' in {1}. Do you want to open this file or folder?", getPathLabel(uri.fsPath, this.environmentMainService), this.productService.nameShort), + message: localize('confirmOpenMessage', "An external application wants to open '{0}' in {1}. Do you want to open this file or folder?", getPathLabel(uri, { os: OS, tildify: this.environmentMainService }), this.productService.nameShort), detail: localize('confirmOpenDetail', "If you did not initiate this request, it may represent an attempted attack on your system. Unless you took an explicit action to initiate this request, you should press 'No'"), noLink: true }); @@ -1088,11 +1093,14 @@ export class CodeApplication extends Disposable { // Telemetry type SharedProcessErrorClassification = { - type: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; owner: 'bpasero'; comment: 'The type of shared process crash to understand the nature of the crash better.' }; - reason: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; owner: 'bpasero'; comment: 'The type of shared process crash to understand the nature of the crash better.' }; - code: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; owner: 'bpasero'; comment: 'The type of shared process crash to understand the nature of the crash better.' }; - visible: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; owner: 'bpasero'; comment: 'Whether shared process window was visible or not.' }; - shuttingdown: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; owner: 'bpasero'; comment: 'Whether the application is shutting down when the crash happens.' }; + type: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'The type of shared process crash to understand the nature of the crash better.' }; + reason: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'The type of shared process crash to understand the nature of the crash better.' }; + code: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'The type of shared process crash to understand the nature of the crash better.' }; + visible: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Whether shared process window was visible or not.' }; + shuttingdown: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Whether the application is shutting down when the crash happens.' }; + owner: 'bpaser'; + comment: 'Event which fires whenever an error occurs in the shared process'; + }; type SharedProcessErrorEvent = { type: WindowError; @@ -1133,7 +1141,7 @@ export class CodeApplication extends Disposable { // Initialize update service const updateService = accessor.get(IUpdateService); if (updateService instanceof Win32UpdateService || updateService instanceof LinuxUpdateService || updateService instanceof DarwinUpdateService) { - updateService.initialize(); + await updateService.initialize(); } // Start to fetch shell environment (if needed) after window has opened diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index 6bed7a06fee..762f288daba 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -3,8 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import 'vs/platform/update/common/update.config.contribution'; + import { app, dialog } from 'electron'; import { unlinkSync } from 'fs'; +import { URI } from 'vs/base/common/uri'; import { coalesce, distinct } from 'vs/base/common/arrays'; import { Promises } from 'vs/base/common/async'; import { toErrorMessage } from 'vs/base/common/errorMessage'; @@ -15,7 +18,7 @@ import { getPathLabel, mnemonicButtonLabel } from 'vs/base/common/labels'; import { Schemas } from 'vs/base/common/network'; import { basename, join, resolve } from 'vs/base/common/path'; import { mark } from 'vs/base/common/performance'; -import { IProcessEnvironment, isMacintosh, isWindows } from 'vs/base/common/platform'; +import { IProcessEnvironment, isMacintosh, isWindows, OS } from 'vs/base/common/platform'; import { cwd } from 'vs/base/common/process'; import { rtrim, trim } from 'vs/base/common/strings'; import { Promises as FSPromises } from 'vs/base/node/pfs'; @@ -59,7 +62,6 @@ import { IStateMainService } from 'vs/platform/state/electron-main/state'; import { StateMainService } from 'vs/platform/state/electron-main/stateMainService'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { IThemeMainService, ThemeMainService } from 'vs/platform/theme/electron-main/themeMainService'; -import 'vs/platform/update/common/update.config.contribution'; /** * The main VS Code entry point. @@ -364,7 +366,7 @@ class CodeMain { private handleStartupDataDirError(environmentMainService: IEnvironmentMainService, title: string, error: NodeJS.ErrnoException): void { if (error.code === 'EACCES' || error.code === 'EPERM') { - const directories = coalesce([environmentMainService.userDataPath, environmentMainService.extensionsPath, XDG_RUNTIME_DIR]).map(folder => getPathLabel(folder, environmentMainService)); + const directories = coalesce([environmentMainService.userDataPath, environmentMainService.extensionsPath, XDG_RUNTIME_DIR]).map(folder => getPathLabel(URI.file(folder), { os: OS, tildify: environmentMainService })); this.showStartupWarningDialog( localize('startupDataDirError', "Unable to write program user data."), diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index 10e33dbfa7c..9495713716e 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -25,7 +25,9 @@ import { NativeEnvironmentService } from 'vs/platform/environment/node/environme import { ExtensionGalleryServiceWithNoStorageService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; import { IExtensionGalleryService, IExtensionManagementCLIService, IExtensionManagementService, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionManagementCLIService } from 'vs/platform/extensionManagement/common/extensionManagementCLIService'; +import { IExtensionsScannerService } from 'vs/platform/extensionManagement/common/extensionsScannerService'; import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; +import { ExtensionsScannerService } from 'vs/platform/extensionManagement/node/extensionsScannerService'; import { IFileService } from 'vs/platform/files/common/files'; import { FileService } from 'vs/platform/files/common/fileService'; import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; @@ -143,6 +145,7 @@ class CliMain extends Disposable { services.set(IDownloadService, new SyncDescriptor(DownloadService)); // Extensions + services.set(IExtensionsScannerService, new SyncDescriptor(ExtensionsScannerService)); services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryServiceWithNoStorageService)); services.set(IExtensionManagementCLIService, new SyncDescriptor(ExtensionManagementCLIService)); diff --git a/src/vs/editor/browser/controller/mouseHandler.ts b/src/vs/editor/browser/controller/mouseHandler.ts index dacc3b77a7c..580619a45df 100644 --- a/src/vs/editor/browser/controller/mouseHandler.ts +++ b/src/vs/editor/browser/controller/mouseHandler.ts @@ -115,6 +115,14 @@ export class MouseHandler extends ViewEventHandler { mousePointerId = pointerId; } })); + // The `pointerup` listener registered by `GlobalEditorPointerMoveMonitor` does not get invoked 100% of the times. + // I speculate that this is because the `pointerup` listener is only registered during the `mousedown` event, and perhaps + // the `pointerup` event is already queued for dispatching, which makes it that the new listener doesn't get fired. + // See https://github.com/microsoft/vscode/issues/146486 for repro steps. + // To compensate for that, we simply register here a `pointerup` listener and just communicate it. + this._register(dom.addDisposableListener(this.viewHelper.viewDomNode, dom.EventType.POINTER_UP, (e: PointerEvent) => { + this._mouseDownOperation.onPointerUp(); + })); this._register(mouseEvents.onMouseDown(this.viewHelper.viewDomNode, (e) => this._onMouseDown(e, mousePointerId))); const onMouseWheel = (browserEvent: IMouseWheelEvent) => { @@ -272,7 +280,7 @@ export class MouseHandler extends ViewEventHandler { e.preventDefault(); } else if (targetIsViewZone) { const viewZoneData = t.detail; - if (this.viewHelper.shouldSuppressMouseDownOnViewZone(viewZoneData.viewZoneId)) { + if (shouldHandle && this.viewHelper.shouldSuppressMouseDownOnViewZone(viewZoneData.viewZoneId)) { focus(); this._mouseDownOperation.start(t.type, e, pointerId); e.preventDefault(); @@ -443,6 +451,10 @@ class MouseDownOperation extends Disposable { this._mouseMoveMonitor.stopMonitoring(); } + public onPointerUp(): void { + this._mouseMoveMonitor.stopMonitoring(); + } + public onScrollChanged(): void { if (!this._isActive) { return; diff --git a/src/vs/editor/browser/controller/textAreaHandler.ts b/src/vs/editor/browser/controller/textAreaHandler.ts index 51a29bb819d..a8a0a6d985e 100644 --- a/src/vs/editor/browser/controller/textAreaHandler.ts +++ b/src/vs/editor/browser/controller/textAreaHandler.ts @@ -228,21 +228,28 @@ export class TextAreaHandler extends ViewPart { // We know for a fact that a screen reader is not attached // On OSX, we write the character before the cursor to allow for "long-press" composition // Also on OSX, we write the word before the cursor to allow for the Accessibility Keyboard to give good hints - if (platform.isMacintosh) { - const selection = this._selections[0]; - if (selection.isEmpty()) { - const position = selection.getStartPosition(); - - let textBefore = this._getWordBeforePosition(position); - if (textBefore.length === 0) { - textBefore = this._getCharacterBeforePosition(position); - } - - if (textBefore.length > 0) { - return new TextAreaState(textBefore, textBefore.length, textBefore.length, position, position); - } + const selection = this._selections[0]; + if (platform.isMacintosh && selection.isEmpty()) { + const position = selection.getStartPosition(); + + let textBefore = this._getWordBeforePosition(position); + if (textBefore.length === 0) { + textBefore = this._getCharacterBeforePosition(position); + } + + if (textBefore.length > 0) { + return new TextAreaState(textBefore, textBefore.length, textBefore.length, position, position); } } + + // on Safari, document.execCommand('cut') and document.execCommand('copy') will just not work + // if the textarea has no content selected. So if there is an editor selection, ensure something + // is selected in the textarea. + if (browser.isSafari && !selection.isEmpty()) { + const placeholderText = 'vscode-placeholder'; + return new TextAreaState(placeholderText, 0, placeholderText.length, null, null); + } + return TextAreaState.EMPTY; } diff --git a/src/vs/editor/browser/controller/textAreaInput.ts b/src/vs/editor/browser/controller/textAreaInput.ts index c736116a3a7..3e7e9a3252e 100644 --- a/src/vs/editor/browser/controller/textAreaInput.ts +++ b/src/vs/editor/browser/controller/textAreaInput.ts @@ -652,6 +652,12 @@ class ClipboardEventUtils { } } + if (text.length === 0 && metadata === null && clipboardData.files.length > 0) { + // no textual data pasted, generate text from file names + const files: File[] = Array.prototype.slice.call(clipboardData.files, 0); + return [files.map(file => file.name).join('\n'), null]; + } + return [text, metadata]; } diff --git a/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts b/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts index 489de25caf0..68ac4e98056 100644 --- a/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts +++ b/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts @@ -190,7 +190,7 @@ export class IndentGuidesOverlay extends DynamicViewOverlay { let activeIndentEndLineNumber = 0; let activeIndentLevel = 0; - if (this._bracketPairGuideOptions.highlightActiveIndentation && activeCursorPosition) { + if (this._bracketPairGuideOptions.highlightActiveIndentation !== false && activeCursorPosition) { const activeIndentInfo = this._context.viewModel.getActiveIndentGuide(activeCursorPosition.lineNumber, visibleStartLineNumber, visibleEndLineNumber); activeIndentStartLineNumber = activeIndentInfo.startLineNumber; activeIndentEndLineNumber = activeIndentInfo.endLineNumber; @@ -213,7 +213,7 @@ export class IndentGuidesOverlay extends DynamicViewOverlay { const indentGuide = (indentLvl - 1) * indentSize + 1; const isActive = // Disable active indent guide if there are bracket guides. - bracketGuidesInLine.length === 0 && + (this._bracketPairGuideOptions.highlightActiveIndentation === 'always' || bracketGuidesInLine.length === 0) && activeIndentStartLineNumber <= lineNumber && lineNumber <= activeIndentEndLineNumber && indentLvl === activeIndentLevel; diff --git a/src/vs/editor/browser/widget/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditorWidget.ts index 22af65a26f5..a29e3b980fc 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget.ts @@ -2333,7 +2333,7 @@ class InlineViewZonesComputer extends ViewZonesComputer { let viewLineCounts: number[] | null = null; for (let lineNumber = lineChange.originalStartLineNumber; lineNumber <= lineChange.originalEndLineNumber; lineNumber++) { const lineIndex = lineNumber - lineChange.originalStartLineNumber; - const lineTokens = this._originalModel.getLineTokens(lineNumber); + const lineTokens = this._originalModel.tokenization.getLineTokens(lineNumber); const lineContent = lineTokens.getLineContent(); const lineBreakData = lineBreaks[lineBreakIndex++]; const actualDecorations = LineDecoration.filter(decorations, lineNumber, 1, lineContent.length + 1); diff --git a/src/vs/editor/common/commands/shiftCommand.ts b/src/vs/editor/common/commands/shiftCommand.ts index 1352d7a60f9..81f552ecd1f 100644 --- a/src/vs/editor/common/commands/shiftCommand.ts +++ b/src/vs/editor/common/commands/shiftCommand.ts @@ -146,7 +146,7 @@ export class ShiftCommand implements ICommand { if (contentStartVisibleColumn % indentSize !== 0) { // The current line is "miss-aligned", so let's see if this is expected... // This can only happen when it has trailing commas in the indent - if (model.isCheapToTokenize(lineNumber - 1)) { + if (model.tokenization.isCheapToTokenize(lineNumber - 1)) { const enterAction = getEnterAction(this._opts.autoIndent, model, new Range(lineNumber - 1, model.getLineMaxColumn(lineNumber - 1), lineNumber - 1, model.getLineMaxColumn(lineNumber - 1)), this._languageConfigurationService); if (enterAction) { extraSpaces = previousLineExtraSpaces; diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 2d38b52d373..ef7d97b40fd 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -2501,7 +2501,7 @@ export interface IEditorInlayHintsOptions { * Enable the inline hints. * Defaults to true. */ - enabled?: boolean; + enabled?: 'on' | 'off' | 'offUnlessPressed' | 'onUnlessPressed'; /** * Font size of inline hints. @@ -2531,14 +2531,21 @@ export type EditorInlayHintsOptions = Readonly<Required<IEditorInlayHintsOptions class EditorInlayHints extends BaseEditorOption<EditorOption.inlayHints, IEditorInlayHintsOptions, EditorInlayHintsOptions> { constructor() { - const defaults: EditorInlayHintsOptions = { enabled: true, fontSize: 0, fontFamily: '', displayStyle: 'compact' }; + const defaults: EditorInlayHintsOptions = { enabled: 'on', fontSize: 0, fontFamily: '', displayStyle: 'compact' }; super( EditorOption.inlayHints, 'inlayHints', defaults, { 'editor.inlayHints.enabled': { - type: 'boolean', + type: 'string', default: defaults.enabled, - description: nls.localize('inlayHints.enable', "Enables the inlay hints in the editor.") + description: nls.localize('inlayHints.enable', "Enables the inlay hints in the editor."), + enum: ['on', 'onUnlessPressed', 'offUnlessPressed', 'off'], + markdownEnumDescriptions: [ + nls.localize('editor.inlayHints.on', "Inlay hints are enabled"), + nls.localize('editor.inlayHints.onUnlessPressed', "Inlay hints are showing by default and hide when holding `Ctrl+Alt`"), + nls.localize('editor.inlayHints.offUnlessPressed', "Inlay hints are hidden by default and show when holding `Ctrl+Alt`"), + nls.localize('editor.inlayHints.off', "Inlay hints are disabled"), + ], }, 'editor.inlayHints.fontSize': { type: 'number', @@ -2550,16 +2557,16 @@ class EditorInlayHints extends BaseEditorOption<EditorOption.inlayHints, IEditor default: defaults.fontFamily, markdownDescription: nls.localize('inlayHints.fontFamily', "Controls font family of inlay hints in the editor. When set to empty, the `#editor.fontFamily#` is used.") }, - 'editor.inlayHints.displayStyle': { - type: 'string', - enum: ['standard', 'compact'], - enumDescriptions: [ - nls.localize('inlayHints.displayStyle.standard', "Renders inlay hints with the default style."), - nls.localize('inlayHints.displayStyle.compact', "Renders inlay hints without any padding, and removes the rounded borders."), - ], - default: defaults.displayStyle, - description: nls.localize('inlayHints.displayStyle', "Controls the display style of inlay hints.") - } + // 'editor.inlayHints.displayStyle': { + // type: 'string', + // enum: ['standard', 'compact'], + // enumDescriptions: [ + // nls.localize('inlayHints.displayStyle.standard', "Renders inlay hints with the default style."), + // nls.localize('inlayHints.displayStyle.compact', "Renders inlay hints without any padding, and removes the rounded borders."), + // ], + // default: defaults.displayStyle, + // description: nls.localize('inlayHints.displayStyle', "Controls the display style of inlay hints.") + // } } ); } @@ -2569,8 +2576,11 @@ class EditorInlayHints extends BaseEditorOption<EditorOption.inlayHints, IEditor return this.defaultValue; } const input = _input as IEditorInlayHintsOptions; + if (typeof input.enabled === 'boolean') { + input.enabled = input.enabled ? 'on' : 'off'; + } return { - enabled: boolean(input.enabled, this.defaultValue.enabled), + enabled: stringSet<'on' | 'off' | 'offUnlessPressed' | 'onUnlessPressed'>(input.enabled, this.defaultValue.enabled, ['on', 'off', 'offUnlessPressed', 'onUnlessPressed']), fontSize: EditorIntOption.clampedInt(input.fontSize, this.defaultValue.fontSize, 0, 100), fontFamily: EditorStringOption.string(input.fontFamily, this.defaultValue.fontFamily), displayStyle: stringSet<'standard' | 'compact'>(input.displayStyle, this.defaultValue.displayStyle, ['standard', 'compact']) @@ -3677,7 +3687,7 @@ export interface IGuidesOptions { * Enable highlighting of the active indent guide. * Defaults to true. */ - highlightActiveIndentation?: boolean; + highlightActiveIndentation?: boolean | 'always'; } /** @@ -3735,8 +3745,15 @@ class GuideOptions extends BaseEditorOption<EditorOption.guides, IGuidesOptions, description: nls.localize('editor.guides.indentation', "Controls whether the editor should render indent guides.") }, 'editor.guides.highlightActiveIndentation': { - type: 'boolean', + type: ['boolean', 'string'], + enum: [true, 'always', false], + enumDescriptions: [ + nls.localize('editor.guides.highlightActiveIndentation.true', "Highlights the active indent guide."), + nls.localize('editor.guides.highlightActiveIndentation.always', "Highlights the active indent guide even if bracket guides are highlighted."), + nls.localize('editor.guides.highlightActiveIndentation.false', "Do not highlight the active indent guide."), + ], default: defaults.highlightActiveIndentation, + description: nls.localize('editor.guides.highlightActiveIndentation', "Controls whether the editor should highlight the active indent guide.") } } @@ -3754,7 +3771,7 @@ class GuideOptions extends BaseEditorOption<EditorOption.guides, IGuidesOptions, highlightActiveBracketPair: boolean(input.highlightActiveBracketPair, this.defaultValue.highlightActiveBracketPair), indentation: boolean(input.indentation, this.defaultValue.indentation), - highlightActiveIndentation: boolean(input.highlightActiveIndentation, this.defaultValue.highlightActiveIndentation), + highlightActiveIndentation: primitiveSet(input.highlightActiveIndentation, this.defaultValue.highlightActiveIndentation, [true, false, 'always']), }; } } diff --git a/src/vs/editor/common/core/editorColorRegistry.ts b/src/vs/editor/common/core/editorColorRegistry.ts index e44ca26bea2..5460cb16e56 100644 --- a/src/vs/editor/common/core/editorColorRegistry.ts +++ b/src/vs/editor/common/core/editorColorRegistry.ts @@ -85,7 +85,13 @@ export const editorUnicodeHighlightBackground = registerColor('editorUnicodeHigh registerThemingParticipant((theme, collector) => { const background = theme.getColor(editorBackground); if (background) { - collector.addRule(`.monaco-editor, .monaco-editor-background, .monaco-editor .inputarea.ime-input { background-color: ${background}; }`); + collector.addRule(`.monaco-editor, .monaco-editor-background { background-color: ${background}; }`); + } + + const lineHighlight = theme.getColor(editorLineHighlight); + const imeBackground = (lineHighlight && !lineHighlight.isTransparent() ? lineHighlight : background); + if (imeBackground) { + collector.addRule(`.monaco-editor .inputarea.ime-input { background-color: ${imeBackground}; }`); } const foreground = theme.getColor(editorForeground); diff --git a/src/vs/editor/common/core/textModelDefaults.ts b/src/vs/editor/common/core/textModelDefaults.ts index f615138fe1d..cb0150baacf 100644 --- a/src/vs/editor/common/core/textModelDefaults.ts +++ b/src/vs/editor/common/core/textModelDefaults.ts @@ -11,7 +11,7 @@ export const EDITOR_MODEL_DEFAULTS = { trimAutoWhitespace: true, largeFileOptimizations: true, bracketPairColorizationOptions: { - enabled: false, + enabled: true, independentColorPoolPerBracketType: false, }, }; diff --git a/src/vs/editor/common/cursor/cursorTypeOperations.ts b/src/vs/editor/common/cursor/cursorTypeOperations.ts index d53f8d89ccf..1048cb59a9d 100644 --- a/src/vs/editor/common/cursor/cursorTypeOperations.ts +++ b/src/vs/editor/common/cursor/cursorTypeOperations.ts @@ -227,7 +227,7 @@ export class TypeOperations { const lineText = model.getLineContent(selection.startLineNumber); - if (/^\s*$/.test(lineText) && model.isCheapToTokenize(selection.startLineNumber)) { + if (/^\s*$/.test(lineText) && model.tokenization.isCheapToTokenize(selection.startLineNumber)) { let goodIndent = this._goodIndentForLine(config, model, selection.startLineNumber); goodIndent = goodIndent || '\t'; const possibleTypeText = config.normalizeIndentation(goodIndent); @@ -300,7 +300,7 @@ export class TypeOperations { if (config.autoIndent === EditorAutoIndentStrategy.None) { return TypeOperations._typeCommand(range, '\n', keepPosition); } - if (!model.isCheapToTokenize(range.getStartPosition().lineNumber) || config.autoIndent === EditorAutoIndentStrategy.Keep) { + if (!model.tokenization.isCheapToTokenize(range.getStartPosition().lineNumber) || config.autoIndent === EditorAutoIndentStrategy.Keep) { const lineText = model.getLineContent(range.startLineNumber); const indentation = strings.getLeadingWhitespace(lineText).substring(0, range.startColumn - 1); return TypeOperations._typeCommand(range, '\n' + config.normalizeIndentation(indentation), keepPosition); @@ -385,7 +385,7 @@ export class TypeOperations { } for (let i = 0, len = selections.length; i < len; i++) { - if (!model.isCheapToTokenize(selections[i].getEndPosition().lineNumber)) { + if (!model.tokenization.isCheapToTokenize(selections[i].getEndPosition().lineNumber)) { return false; } } @@ -642,13 +642,13 @@ export class TypeOperations { } } - if (!model.isCheapToTokenize(lineNumber)) { + if (!model.tokenization.isCheapToTokenize(lineNumber)) { // Do not force tokenization return null; } - model.forceTokenization(lineNumber); - const lineTokens = model.getLineTokens(lineNumber); + model.tokenization.forceTokenization(lineNumber); + const lineTokens = model.tokenization.getLineTokens(lineNumber); const scopedLineTokens = createScopedLineTokens(lineTokens, beforeColumn - 1); if (!pair.shouldAutoClose(scopedLineTokens, beforeColumn - scopedLineTokens.firstCharOffset)) { return null; @@ -664,7 +664,7 @@ export class TypeOperations { // const neutralCharacter = pair.findNeutralCharacter(); if (neutralCharacter) { - const tokenType = model.getTokenTypeIfInsertingCharacter(lineNumber, beforeColumn, neutralCharacter); + const tokenType = model.tokenization.getTokenTypeIfInsertingCharacter(lineNumber, beforeColumn, neutralCharacter); if (!pair.isOK(tokenType)) { return null; } @@ -757,7 +757,7 @@ export class TypeOperations { } private static _isTypeInterceptorElectricChar(config: CursorConfiguration, model: ITextModel, selections: Selection[]) { - if (selections.length === 1 && model.isCheapToTokenize(selections[0].getEndPosition().lineNumber)) { + if (selections.length === 1 && model.tokenization.isCheapToTokenize(selections[0].getEndPosition().lineNumber)) { return true; } return false; @@ -769,8 +769,8 @@ export class TypeOperations { } const position = selection.getPosition(); - model.forceTokenization(position.lineNumber); - const lineTokens = model.getLineTokens(position.lineNumber); + model.tokenization.forceTokenization(position.lineNumber); + const lineTokens = model.tokenization.getLineTokens(position.lineNumber); let electricAction: IElectricAction | null; try { diff --git a/src/vs/editor/common/languageSelector.ts b/src/vs/editor/common/languageSelector.ts index 2ef0be7ee10..64a8440059f 100644 --- a/src/vs/editor/common/languageSelector.ts +++ b/src/vs/editor/common/languageSelector.ts @@ -87,7 +87,7 @@ export function score(selector: LanguageSelector | undefined, candidateUri: URI, if (notebookType) { if (notebookType === candidateNotebookType) { ret = 10; - } else if (notebookType === '*') { + } else if (notebookType === '*' && candidateNotebookType !== undefined) { ret = Math.max(ret, 5); } else { return 0; diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index de092a4d1be..fedc753fb66 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -75,10 +75,11 @@ export const enum StandardTokenType { * 1098 7654 3210 9876 5432 1098 7654 3210 * - ------------------------------------------- * xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx - * bbbb bbbb bfff ffff ffFF FFTT LLLL LLLL + * bbbb bbbb ffff ffff fFFF FBTT LLLL LLLL * - ------------------------------------------- * - L = LanguageId (8 bits) * - T = StandardTokenType (2 bits) + * - B = Balanced bracket (1 bit) * - F = FontStyle (4 bits) * - f = foreground color (9 bits) * - b = background color (9 bits) @@ -88,14 +89,15 @@ export const enum StandardTokenType { export const enum MetadataConsts { LANGUAGEID_MASK = 0b00000000000000000000000011111111, TOKEN_TYPE_MASK = 0b00000000000000000000001100000000, - FONT_STYLE_MASK = 0b00000000000000000011110000000000, - FOREGROUND_MASK = 0b00000000011111111100000000000000, - BACKGROUND_MASK = 0b11111111100000000000000000000000, + BALANCED_BRACKETS_MASK = 0b00000000000000000000010000000000, + FONT_STYLE_MASK = 0b00000000000000000111100000000000, + FOREGROUND_MASK = 0b00000000111111111000000000000000, + BACKGROUND_MASK = 0b11111111000000000000000000000000, - ITALIC_MASK = 0b00000000000000000000010000000000, - BOLD_MASK = 0b00000000000000000000100000000000, - UNDERLINE_MASK = 0b00000000000000000001000000000000, - STRIKETHROUGH_MASK = 0b00000000000000000010000000000000, + ITALIC_MASK = 0b00000000000000000000100000000000, + BOLD_MASK = 0b00000000000000000001000000000000, + UNDERLINE_MASK = 0b00000000000000000010000000000000, + STRIKETHROUGH_MASK = 0b00000000000000000100000000000000, // Semantic tokens cannot set the language id, so we can // use the first 8 bits for control purposes @@ -108,9 +110,10 @@ export const enum MetadataConsts { LANGUAGEID_OFFSET = 0, TOKEN_TYPE_OFFSET = 8, - FONT_STYLE_OFFSET = 10, - FOREGROUND_OFFSET = 14, - BACKGROUND_OFFSET = 23 + BALANCED_BRACKETS_OFFSET = 10, + FONT_STYLE_OFFSET = 11, + FOREGROUND_OFFSET = 15, + BACKGROUND_OFFSET = 24 } /** @@ -126,6 +129,10 @@ export class TokenMetadata { return (metadata & MetadataConsts.TOKEN_TYPE_MASK) >>> MetadataConsts.TOKEN_TYPE_OFFSET; } + public static containsBalancedBrackets(metadata: number): boolean { + return (metadata & MetadataConsts.BALANCED_BRACKETS_MASK) !== 0; + } + public static getFontStyle(metadata: number): FontStyle { return (metadata & MetadataConsts.FONT_STYLE_MASK) >>> MetadataConsts.FONT_STYLE_OFFSET; } @@ -673,6 +680,10 @@ export interface CompletionItem { * A command that should be run upon acceptance of this item. */ command?: Command; + /** + * @internal + */ + extensionId?: ExtensionIdentifier; /** * @internal @@ -822,6 +833,10 @@ export interface InlineCompletion { export interface InlineCompletions<TItem extends InlineCompletion = InlineCompletion> { readonly items: readonly TItem[]; + /** + * A list of commands associated with the inline completions of this list. + */ + readonly commands?: Command[]; } export interface InlineCompletionsProvider<T extends InlineCompletions = InlineCompletions> { diff --git a/src/vs/editor/common/languages/autoIndent.ts b/src/vs/editor/common/languages/autoIndent.ts index d588ea766a8..44c9b30467d 100644 --- a/src/vs/editor/common/languages/autoIndent.ts +++ b/src/vs/editor/common/languages/autoIndent.ts @@ -14,9 +14,11 @@ import { getScopedLineTokens, ILanguageConfigurationService } from 'vs/editor/co import { LineTokens } from 'vs/editor/common/tokens/lineTokens'; export interface IVirtualModel { - getLineTokens(lineNumber: number): LineTokens; - getLanguageId(): string; - getLanguageIdAtPosition(lineNumber: number, column: number): string; + tokenization: { + getLineTokens(lineNumber: number): LineTokens; + getLanguageId(): string; + getLanguageIdAtPosition(lineNumber: number, column: number): string; + }; getLineContent(lineNumber: number): string; } @@ -34,13 +36,13 @@ export interface IIndentConverter { * else: nearest preceding line of the same language */ function getPrecedingValidLine(model: IVirtualModel, lineNumber: number, indentRulesSupport: IndentRulesSupport) { - const languageId = model.getLanguageIdAtPosition(lineNumber, 0); + const languageId = model.tokenization.getLanguageIdAtPosition(lineNumber, 0); if (lineNumber > 1) { let lastLineNumber: number; let resultLineNumber = -1; for (lastLineNumber = lineNumber - 1; lastLineNumber >= 1; lastLineNumber--) { - if (model.getLanguageIdAtPosition(lastLineNumber, 0) !== languageId) { + if (model.tokenization.getLanguageIdAtPosition(lastLineNumber, 0) !== languageId) { return resultLineNumber; } const text = model.getLineContent(lastLineNumber); @@ -79,7 +81,7 @@ export function getInheritIndentForLine( return null; } - const indentRulesSupport = languageConfigurationService.getLanguageConfiguration(model.getLanguageId()).indentRulesSupport; + const indentRulesSupport = languageConfigurationService.getLanguageConfiguration(model.tokenization.getLanguageId()).indentRulesSupport; if (!indentRulesSupport) { return null; } @@ -283,8 +285,8 @@ export function getIndentForEnter( if (autoIndent < EditorAutoIndentStrategy.Full) { return null; } - model.forceTokenization(range.startLineNumber); - const lineTokens = model.getLineTokens(range.startLineNumber); + model.tokenization.forceTokenization(range.startLineNumber); + const lineTokens = model.tokenization.getLineTokens(range.startLineNumber); const scopedLineTokens = createScopedLineTokens(lineTokens, range.startColumn - 1); const scopedLineText = scopedLineTokens.getLineContent(); @@ -315,14 +317,16 @@ export function getIndentForEnter( const beforeEnterIndent = strings.getLeadingWhitespace(beforeEnterText); const virtualModel: IVirtualModel = { - getLineTokens: (lineNumber: number) => { - return model.getLineTokens(lineNumber); - }, - getLanguageId: () => { - return model.getLanguageId(); - }, - getLanguageIdAtPosition: (lineNumber: number, column: number) => { - return model.getLanguageIdAtPosition(lineNumber, column); + tokenization: { + getLineTokens: (lineNumber: number) => { + return model.tokenization.getLineTokens(lineNumber); + }, + getLanguageId: () => { + return model.getLanguageId(); + }, + getLanguageIdAtPosition: (lineNumber: number, column: number) => { + return model.getLanguageIdAtPosition(lineNumber, column); + }, }, getLineContent: (lineNumber: number) => { if (lineNumber === range.startLineNumber) { diff --git a/src/vs/editor/common/languages/languageConfigurationRegistry.ts b/src/vs/editor/common/languages/languageConfigurationRegistry.ts index 2b641d53c0e..64713283a3e 100644 --- a/src/vs/editor/common/languages/languageConfigurationRegistry.ts +++ b/src/vs/editor/common/languages/languageConfigurationRegistry.ts @@ -21,6 +21,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { ILanguageService } from 'vs/editor/common/languages/language'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; +import { LanguageBracketsConfiguration } from 'vs/editor/common/languages/supports/languageBracketsConfiguration'; /** * Interface used to support insertion of mode specific comments. @@ -179,8 +180,8 @@ export function getIndentationAtPosition(model: ITextModel, lineNumber: number, } export function getScopedLineTokens(model: ITextModel, lineNumber: number, columnNumber?: number): ScopedLineTokens { - model.forceTokenization(lineNumber); - const lineTokens = model.getLineTokens(lineNumber); + model.tokenization.forceTokenization(lineNumber); + const lineTokens = model.tokenization.getLineTokens(lineNumber); const column = (typeof columnNumber === 'undefined' ? model.getLineMaxColumn(lineNumber) - 1 : columnNumber - 1); return createScopedLineTokens(lineTokens, column); } @@ -363,6 +364,7 @@ export class ResolvedLanguageConfiguration { public readonly indentRulesSupport: IndentRulesSupport | null; public readonly indentationRules: IndentationRule | undefined; public readonly foldingRules: FoldingRules; + public readonly bracketsNew: LanguageBracketsConfiguration; constructor( public readonly languageId: string, @@ -389,6 +391,11 @@ export class ResolvedLanguageConfiguration { this.indentRulesSupport = null; } this.foldingRules = this.underlyingConfig.folding || {}; + + this.bracketsNew = new LanguageBracketsConfiguration( + languageId, + this.underlyingConfig + ); } public getWordDefinition(): RegExp { diff --git a/src/vs/editor/common/languages/supports/characterPair.ts b/src/vs/editor/common/languages/supports/characterPair.ts index 29503d7a894..9ee152e7519 100644 --- a/src/vs/editor/common/languages/supports/characterPair.ts +++ b/src/vs/editor/common/languages/supports/characterPair.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IAutoClosingPair, StandardAutoClosingPairConditional, LanguageConfiguration, CharacterPair } from 'vs/editor/common/languages/languageConfiguration'; +import { IAutoClosingPair, StandardAutoClosingPairConditional, LanguageConfiguration } from 'vs/editor/common/languages/languageConfiguration'; export class CharacterPairSupport { @@ -13,7 +13,6 @@ export class CharacterPairSupport { private readonly _autoClosingPairs: StandardAutoClosingPairConditional[]; private readonly _surroundingPairs: IAutoClosingPair[]; private readonly _autoCloseBefore: string; - private readonly _colorizedBracketPairs: CharacterPair[]; constructor(config: LanguageConfiguration) { if (config.autoClosingPairs) { @@ -24,20 +23,6 @@ export class CharacterPairSupport { this._autoClosingPairs = []; } - if (config.colorizedBracketPairs) { - this._colorizedBracketPairs = filterValidBrackets(config.colorizedBracketPairs.map(b => [b[0], b[1]])); - } else if (config.brackets) { - this._colorizedBracketPairs = filterValidBrackets(config.brackets - .map((b) => [b[0], b[1]] as [string, string]) - // Many languages set < ... > as bracket pair, even though they also use it as comparison operator. - // This leads to problems when colorizing this bracket, so we exclude it by default. - // Languages can still override this by configuring `colorizedBracketPairs` - // https://github.com/microsoft/vscode/issues/132476 - .filter((p) => !(p[0] === '<' && p[1] === '>'))); - } else { - this._colorizedBracketPairs = []; - } - if (config.__electricCharacterSupport && config.__electricCharacterSupport.docComment) { const docComment = config.__electricCharacterSupport.docComment; // IDocComment is legacy, only partially supported @@ -60,12 +45,4 @@ export class CharacterPairSupport { public getSurroundingPairs(): IAutoClosingPair[] { return this._surroundingPairs; } - - public getColorizedBrackets(): readonly CharacterPair[] { - return this._colorizedBracketPairs; - } -} - -function filterValidBrackets(bracketPairs: [string, string][]): [string, string][] { - return bracketPairs.filter(([open, close]) => open !== '' && close !== ''); } diff --git a/src/vs/editor/common/languages/supports/languageBracketsConfiguration.ts b/src/vs/editor/common/languages/supports/languageBracketsConfiguration.ts new file mode 100644 index 00000000000..79f92c6ed52 --- /dev/null +++ b/src/vs/editor/common/languages/supports/languageBracketsConfiguration.ts @@ -0,0 +1,155 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CachedFunction } from 'vs/base/common/cache'; +import { BugIndicatingError } from 'vs/base/common/errors'; +import { LanguageConfiguration } from 'vs/editor/common/languages/languageConfiguration'; + +/** + * Captures all bracket related configurations for a single language. + * Immutable. +*/ +export class LanguageBracketsConfiguration { + private readonly _openingBrackets: ReadonlyMap<string, OpeningBracketKind>; + private readonly _closingBrackets: ReadonlyMap<string, ClosingBracketKind>; + + constructor( + public readonly languageId: string, + config: LanguageConfiguration, + ) { + let brackets: [string, string][]; + + // Prefer colorized bracket pairs, as they are more accurate. + // TODO@hediet: Deprecate `colorizedBracketPairs` and increase accuracy for brackets. + if (config.colorizedBracketPairs) { + brackets = filterValidBrackets(config.colorizedBracketPairs.map(b => [b[0], b[1]])); + } else if (config.brackets) { + brackets = filterValidBrackets(config.brackets + .map((b) => [b[0], b[1]] as [string, string]) + // Many languages set < ... > as bracket pair, even though they also use it as comparison operator. + // This leads to problems when colorizing this bracket, so we exclude it by default. + // Languages can still override this by configuring `colorizedBracketPairs` + // https://github.com/microsoft/vscode/issues/132476 + .filter((p) => !(p[0] === '<' && p[1] === '>'))); + } else { + brackets = []; + } + + const openingBracketInfos = new CachedFunction((bracket: string) => { + const closing = new Set<ClosingBracketKind>(); + return { + info: new OpeningBracketKind(this, bracket, closing), + closing, + }; + }); + const closingBracketInfos = new CachedFunction((bracket: string) => { + const opening = new Set<OpeningBracketKind>(); + return { + info: new ClosingBracketKind(this, bracket, opening), + opening, + }; + }); + + for (const [open, close] of brackets) { + const opening = openingBracketInfos.get(open); + const closing = closingBracketInfos.get(close); + + opening.closing.add(closing.info); + closing.opening.add(opening.info); + } + + this._openingBrackets = new Map([...openingBracketInfos.cachedValues].map(([k, v]) => [k, v.info])); + this._closingBrackets = new Map([...closingBracketInfos.cachedValues].map(([k, v]) => [k, v.info])); + } + + /** + * No two brackets have the same bracket text. + */ + public get openingBrackets(): readonly OpeningBracketKind[] { + return [...this._openingBrackets.values()]; + } + + /** + * No two brackets have the same bracket text. + */ + public get closingBrackets(): readonly ClosingBracketKind[] { + return [...this._closingBrackets.values()]; + } + + public getOpeningBracketInfo(bracketText: string): OpeningBracketKind | undefined { + return this._openingBrackets.get(bracketText); + } + + public getClosingBracketInfo(bracketText: string): ClosingBracketKind | undefined { + return this._closingBrackets.get(bracketText); + } + + public getBracketInfo(bracketText: string): BracketKind | undefined { + return this.getOpeningBracketInfo(bracketText) || this.getClosingBracketInfo(bracketText); + } +} + +function filterValidBrackets(bracketPairs: [string, string][]): [string, string][] { + return bracketPairs.filter(([open, close]) => open !== '' && close !== ''); +} + +export type BracketKind = OpeningBracketKind | ClosingBracketKind; + +export class BracketKindBase { + constructor( + protected readonly config: LanguageBracketsConfiguration, + public readonly bracketText: string, + ) { } + + public get languageId(): string { + return this.config.languageId; + } +} + +export class OpeningBracketKind extends BracketKindBase { + public readonly isOpeningBracket = true; + + constructor( + config: LanguageBracketsConfiguration, + bracketText: string, + public readonly openedBrackets: ReadonlySet<ClosingBracketKind> + ) { + super(config, bracketText); + } +} + +export class ClosingBracketKind extends BracketKindBase { + public readonly isOpeningBracket = false; + + constructor( + config: LanguageBracketsConfiguration, + bracketText: string, + /** + * Non empty array of all opening brackets this bracket closes. + */ + public readonly closedBrackets: ReadonlySet<OpeningBracketKind> + ) { + super(config, bracketText); + } + + /** + * Checks if this bracket closes the given other bracket. + * Brackets from other language configuration can be used (they will always return false). + * If other is a bracket with the same language id, they have to be from the same configuration. + */ + public closes(other: OpeningBracketKind): boolean { + if (other.languageId === this.languageId) { + if (other['config'] !== this.config) { + throw new BugIndicatingError('Brackets from different language configuration cannot be used.'); + } + } + + return this.closedBrackets.has(other); + } + + public getClosedBrackets(): readonly OpeningBracketKind[] { + return [...this.closedBrackets]; + } +} diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index 4d3ec0b94c8..b0b87e92fbd 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -6,23 +6,21 @@ import { Event } from 'vs/base/common/event'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { IDisposable } from 'vs/base/common/lifecycle'; +import { equals } from 'vs/base/common/objects'; import { URI } from 'vs/base/common/uri'; -import { LineTokens } from 'vs/editor/common/tokens/lineTokens'; +import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; -import { IModelContentChange, IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent, IModelTokensChangedEvent, InternalModelContentChangeEvent, ModelInjectedTextChangedEvent } from 'vs/editor/common/textModelEvents'; -import { WordCharacterClassifier } from 'vs/editor/common/core/wordCharacterClassifier'; -import { FormattingOptions, StandardTokenType } from 'vs/editor/common/languages'; -import { ThemeColor } from 'vs/platform/theme/common/themeService'; -import { ContiguousMultilineTokens } from 'vs/editor/common/tokens/contiguousMultilineTokens'; -import { SparseMultilineTokens } from 'vs/editor/common/tokens/sparseMultilineTokens'; import { TextChange } from 'vs/editor/common/core/textChange'; -import { equals } from 'vs/base/common/objects'; +import { WordCharacterClassifier } from 'vs/editor/common/core/wordCharacterClassifier'; +import { IWordAtPosition } from 'vs/editor/common/core/wordHelper'; +import { FormattingOptions } from 'vs/editor/common/languages'; import { IBracketPairsTextModelPart } from 'vs/editor/common/textModelBracketPairs'; +import { IModelContentChange, IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent, IModelTokensChangedEvent, InternalModelContentChangeEvent, ModelInjectedTextChangedEvent } from 'vs/editor/common/textModelEvents'; import { IGuidesTextModelPart } from 'vs/editor/common/textModelGuides'; -import { IWordAtPosition } from 'vs/editor/common/core/wordHelper'; -import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; +import { ITokenizationTextModelPart } from 'vs/editor/common/tokenizationTextModelPart'; +import { ThemeColor } from 'vs/platform/theme/common/themeService'; /** * Vertical Lane in the overview ruler of the editor. @@ -780,11 +778,6 @@ export interface ITextModel { isDisposed(): boolean; /** - * @internal - */ - tokenizeViewport(startLineNumber: number, endLineNumber: number): void; - - /** * This model is so large that it would not be a good idea to sync it over * to web workers or other places. * @internal @@ -844,63 +837,6 @@ export interface ITextModel { */ findPreviousMatch(searchString: string, searchStart: IPosition, isRegex: boolean, matchCase: boolean, wordSeparators: string | null, captureMatches: boolean): FindMatch | null; - /** - * @internal - */ - setTokens(tokens: ContiguousMultilineTokens[]): void; - - /** - * @internal - */ - setSemanticTokens(tokens: SparseMultilineTokens[] | null, isComplete: boolean): void; - - /** - * @internal - */ - setPartialSemanticTokens(range: Range, tokens: SparseMultilineTokens[] | null): void; - - /** - * @internal - */ - hasCompleteSemanticTokens(): boolean; - - /** - * @internal - */ - hasSomeSemanticTokens(): boolean; - - /** - * Flush all tokenization state. - * @internal - */ - resetTokenization(): void; - - /** - * Force tokenization information for `lineNumber` to be accurate. - * @internal - */ - forceTokenization(lineNumber: number): void; - - /** - * If it is cheap, force tokenization information for `lineNumber` to be accurate. - * This is based on a heuristic. - * @internal - */ - tokenizeIfCheap(lineNumber: number): void; - - /** - * Check if calling `forceTokenization` for this `lineNumber` will be cheap (time-wise). - * This is based on a heuristic. - * @internal - */ - isCheapToTokenize(lineNumber: number): boolean; - - /** - * Get the tokens for the line `lineNumber`. - * The tokens might be inaccurate. Use `forceTokenization` to ensure accurate tokens. - * @internal - */ - getLineTokens(lineNumber: number): LineTokens; /** * Get the language associated with this model. @@ -921,18 +857,6 @@ export interface ITextModel { getLanguageIdAtPosition(lineNumber: number, column: number): string; /** - * Returns the standard token type for a character if the character were to be inserted at - * the given position. If the result cannot be accurate, it returns null. - * @internal - */ - getTokenTypeIfInsertingCharacter(lineNumber: number, column: number, character: string): StandardTokenType; - - /** - * @internal - */ - tokenizeLineWithEdit(position: IPosition, length: number, newText: string): LineTokens | null; - - /** * Get the word under or besides `position`. * @param position The position to look for a word. * @return The word under or besides `position`. Might be null. @@ -1253,6 +1177,11 @@ export interface ITextModel { * @internal */ readonly guides: IGuidesTextModelPart; + + /** + * @internal + */ + readonly tokenization: ITokenizationTextModelPart; } export const enum PositionAffinity { diff --git a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsImpl.ts b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsImpl.ts index 2b89a098fb9..7148cae42f2 100644 --- a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsImpl.ts +++ b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsImpl.ts @@ -15,6 +15,8 @@ import { IModelContentChangedEvent, IModelLanguageChangedEvent, IModelOptionsCha import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { ignoreBracketsInToken } from 'vs/editor/common/languages/supports'; import { RichEditBrackets, BracketsUtils, RichEditBracket } from 'vs/editor/common/languages/supports/richEditBrackets'; +import { compareBy, findLast, findLastMaxBy } from 'vs/base/common/arrays'; +import { LanguageBracketsConfiguration } from 'vs/editor/common/languages/supports/languageBracketsConfiguration'; export class BracketPairsTextModelPart extends Disposable implements IBracketPairsTextModelPart { private readonly bracketPairsTree = this._register(new MutableDisposable<IReference<BracketPairsTree>>()); @@ -22,7 +24,7 @@ export class BracketPairsTextModelPart extends Disposable implements IBracketPai private readonly onDidChangeEmitter = new Emitter<void>(); public readonly onDidChange = this.onDidChangeEmitter.event; - private get isDocumentSupported() { + private get canBuildAST() { const maxSupportedDocumentLength = /* max lines */ 50_000 * /* average column count */ 100; return this.textModel.getValueLength() <= maxSupportedDocumentLength; } @@ -72,7 +74,7 @@ export class BracketPairsTextModelPart extends Disposable implements IBracketPai //#endregion private updateBracketPairsTree() { - if (this.bracketsRequested && this.isDocumentSupported) { + if (this.bracketsRequested && this.canBuildAST) { if (!this.bracketPairsTree.value) { const store = new DisposableStore(); @@ -119,28 +121,70 @@ export class BracketPairsTextModelPart extends Disposable implements IBracketPai } public findMatchingBracketUp(_bracket: string, _position: IPosition, maxDuration?: number): Range | null { - const bracket = _bracket.toLowerCase(); const position = this.textModel.validatePosition(_position); - const languageId = this.textModel.getLanguageIdAtPosition(position.lineNumber, position.column); - const bracketsSupport = this.languageConfigurationService.getLanguageConfiguration(languageId).brackets; - if (!bracketsSupport) { - return null; - } + if (this.canBuildAST) { + const closingBracketInfo = this.languageConfigurationService + .getLanguageConfiguration(languageId) + .bracketsNew.getClosingBracketInfo(_bracket)!; - const data = bracketsSupport.textIsBracket[bracket]; + const bracketPair = findLast(this.getBracketPairsInRange(Range.fromPositions(_position, _position)) || [], (b) => + closingBracketInfo.closes(b.openingBracketInfo) + ); - if (!data) { + if (bracketPair) { + return bracketPair.openingBracketRange; + } return null; - } + } else { + // Fallback to old bracket matching code: + const bracket = _bracket.toLowerCase(); + + const bracketsSupport = this.languageConfigurationService.getLanguageConfiguration(languageId).brackets; + + if (!bracketsSupport) { + return null; + } - return stripBracketSearchCanceled(this._findMatchingBracketUp(data, position, createTimeBasedContinueBracketSearchPredicate(maxDuration))); + const data = bracketsSupport.textIsBracket[bracket]; + + if (!data) { + return null; + } + + return stripBracketSearchCanceled(this._findMatchingBracketUp(data, position, createTimeBasedContinueBracketSearchPredicate(maxDuration))); + } } public matchBracket(position: IPosition, maxDuration?: number): [Range, Range] | null { - const continueSearchPredicate = createTimeBasedContinueBracketSearchPredicate(maxDuration); - return this._matchBracket(this.textModel.validatePosition(position), continueSearchPredicate); + if (this.canBuildAST) { + const bracketPair = findLastMaxBy( + this.getBracketPairsInRange( + Range.fromPositions(position, position) + ).filter( + (item) => + item.closingBracketRange !== undefined && + (item.openingBracketRange.containsPosition(position) || + item.closingBracketRange.containsPosition(position)) + ), + compareBy( + (item) => + item.openingBracketRange.containsPosition(position) + ? item.openingBracketRange + : item.closingBracketRange, + Range.compareRangesUsingStarts + ) + ); + if (bracketPair) { + return [bracketPair.openingBracketRange, bracketPair.closingBracketRange!]; + } + return null; + } else { + // Fallback to old bracket matching code: + const continueSearchPredicate = createTimeBasedContinueBracketSearchPredicate(maxDuration); + return this._matchBracket(this.textModel.validatePosition(position), continueSearchPredicate); + } } private _establishBracketSearchOffsets(position: Position, lineTokens: LineTokens, modeBrackets: RichEditBrackets, tokenIndex: number) { @@ -178,7 +222,7 @@ export class BracketPairsTextModelPart extends Disposable implements IBracketPai private _matchBracket(position: Position, continueSearchPredicate: ContinueBracketSearchPredicate): [Range, Range] | null { const lineNumber = position.lineNumber; - const lineTokens = this.textModel.getLineTokens(lineNumber); + const lineTokens = this.textModel.tokenization.getLineTokens(lineNumber); const lineText = this.textModel.getLineContent(lineNumber); const tokenIndex = lineTokens.findTokenIndexAtOffset(position.column - 1); @@ -309,7 +353,7 @@ export class BracketPairsTextModelPart extends Disposable implements IBracketPai }; for (let lineNumber = position.lineNumber; lineNumber >= 1; lineNumber--) { - const lineTokens = this.textModel.getLineTokens(lineNumber); + const lineTokens = this.textModel.tokenization.getLineTokens(lineNumber); const tokenCount = lineTokens.getCount(); const lineText = this.textModel.getLineContent(lineNumber); @@ -397,7 +441,7 @@ export class BracketPairsTextModelPart extends Disposable implements IBracketPai const lineCount = this.textModel.getLineCount(); for (let lineNumber = position.lineNumber; lineNumber <= lineCount; lineNumber++) { - const lineTokens = this.textModel.getLineTokens(lineNumber); + const lineTokens = this.textModel.tokenization.getLineTokens(lineNumber); const tokenCount = lineTokens.getCount(); const lineText = this.textModel.getLineContent(lineNumber); @@ -451,10 +495,17 @@ export class BracketPairsTextModelPart extends Disposable implements IBracketPai public findPrevBracket(_position: IPosition): IFoundBracket | null { const position = this.textModel.validatePosition(_position); + if (this.canBuildAST) { + this.bracketsRequested = true; + this.updateBracketPairsTree(); + return this.bracketPairsTree.value?.object.getFirstBracketBefore(position) || null; + } + let languageId: string | null = null; let modeBrackets: RichEditBrackets | null = null; + let bracketConfig: LanguageBracketsConfiguration | null = null; for (let lineNumber = position.lineNumber; lineNumber >= 1; lineNumber--) { - const lineTokens = this.textModel.getLineTokens(lineNumber); + const lineTokens = this.textModel.tokenization.getLineTokens(lineNumber); const tokenCount = lineTokens.getCount(); const lineText = this.textModel.getLineContent(lineNumber); @@ -469,6 +520,7 @@ export class BracketPairsTextModelPart extends Disposable implements IBracketPai if (languageId !== tokenLanguageId) { languageId = tokenLanguageId; modeBrackets = this.languageConfigurationService.getLanguageConfiguration(languageId).brackets; + bracketConfig = this.languageConfigurationService.getLanguageConfiguration(languageId).bracketsNew; } } @@ -478,15 +530,16 @@ export class BracketPairsTextModelPart extends Disposable implements IBracketPai if (languageId !== tokenLanguageId) { // language id change! - if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { + if (modeBrackets && bracketConfig && prevSearchInToken && searchStartOffset !== searchEndOffset) { const r = BracketsUtils.findPrevBracketInRange(modeBrackets.reversedRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); if (r) { - return this._toFoundBracket(modeBrackets, r); + return this._toFoundBracket(bracketConfig, r); } prevSearchInToken = false; } languageId = tokenLanguageId; modeBrackets = this.languageConfigurationService.getLanguageConfiguration(languageId).brackets; + bracketConfig = this.languageConfigurationService.getLanguageConfiguration(languageId).bracketsNew; } const searchInToken = (!!modeBrackets && !ignoreBracketsInToken(lineTokens.getStandardTokenType(tokenIndex))); @@ -503,10 +556,10 @@ export class BracketPairsTextModelPart extends Disposable implements IBracketPai } } else { // this token should not be searched - if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { + if (bracketConfig && modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { const r = BracketsUtils.findPrevBracketInRange(modeBrackets.reversedRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); if (r) { - return this._toFoundBracket(modeBrackets, r); + return this._toFoundBracket(bracketConfig, r); } } } @@ -514,10 +567,10 @@ export class BracketPairsTextModelPart extends Disposable implements IBracketPai prevSearchInToken = searchInToken; } - if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { + if (bracketConfig && modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { const r = BracketsUtils.findPrevBracketInRange(modeBrackets.reversedRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); if (r) { - return this._toFoundBracket(modeBrackets, r); + return this._toFoundBracket(bracketConfig, r); } } } @@ -527,12 +580,20 @@ export class BracketPairsTextModelPart extends Disposable implements IBracketPai public findNextBracket(_position: IPosition): IFoundBracket | null { const position = this.textModel.validatePosition(_position); + + if (this.canBuildAST) { + this.bracketsRequested = true; + this.updateBracketPairsTree(); + return this.bracketPairsTree.value?.object.getFirstBracketAfter(position) || null; + } + const lineCount = this.textModel.getLineCount(); let languageId: string | null = null; let modeBrackets: RichEditBrackets | null = null; + let bracketConfig: LanguageBracketsConfiguration | null = null; for (let lineNumber = position.lineNumber; lineNumber <= lineCount; lineNumber++) { - const lineTokens = this.textModel.getLineTokens(lineNumber); + const lineTokens = this.textModel.tokenization.getLineTokens(lineNumber); const tokenCount = lineTokens.getCount(); const lineText = this.textModel.getLineContent(lineNumber); @@ -547,6 +608,7 @@ export class BracketPairsTextModelPart extends Disposable implements IBracketPai if (languageId !== tokenLanguageId) { languageId = tokenLanguageId; modeBrackets = this.languageConfigurationService.getLanguageConfiguration(languageId).brackets; + bracketConfig = this.languageConfigurationService.getLanguageConfiguration(languageId).bracketsNew; } } @@ -556,15 +618,16 @@ export class BracketPairsTextModelPart extends Disposable implements IBracketPai if (languageId !== tokenLanguageId) { // language id change! - if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { + if (bracketConfig && modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { const r = BracketsUtils.findNextBracketInRange(modeBrackets.forwardRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); if (r) { - return this._toFoundBracket(modeBrackets, r); + return this._toFoundBracket(bracketConfig, r); } prevSearchInToken = false; } languageId = tokenLanguageId; modeBrackets = this.languageConfigurationService.getLanguageConfiguration(languageId).brackets; + bracketConfig = this.languageConfigurationService.getLanguageConfiguration(languageId).bracketsNew; } const searchInToken = (!!modeBrackets && !ignoreBracketsInToken(lineTokens.getStandardTokenType(tokenIndex))); @@ -580,10 +643,10 @@ export class BracketPairsTextModelPart extends Disposable implements IBracketPai } } else { // this token should not be searched - if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { + if (bracketConfig && modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { const r = BracketsUtils.findNextBracketInRange(modeBrackets.forwardRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); if (r) { - return this._toFoundBracket(modeBrackets, r); + return this._toFoundBracket(bracketConfig, r); } } } @@ -591,10 +654,10 @@ export class BracketPairsTextModelPart extends Disposable implements IBracketPai prevSearchInToken = searchInToken; } - if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { + if (bracketConfig && modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { const r = BracketsUtils.findNextBracketInRange(modeBrackets.forwardRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); if (r) { - return this._toFoundBracket(modeBrackets, r); + return this._toFoundBracket(bracketConfig, r); } } } @@ -603,8 +666,21 @@ export class BracketPairsTextModelPart extends Disposable implements IBracketPai } public findEnclosingBrackets(_position: IPosition, maxDuration?: number): [Range, Range] | null { - const continueSearchPredicate = createTimeBasedContinueBracketSearchPredicate(maxDuration); const position = this.textModel.validatePosition(_position); + + if (this.canBuildAST) { + const range = Range.fromPositions(position); + const bracketPair = findLast( + this.getBracketPairsInRange(Range.fromPositions(position, position)), + (item) => item.closingBracketRange !== undefined && item.range.strictContainsRange(range) + ); + if (bracketPair) { + return [bracketPair.openingBracketRange, bracketPair.closingBracketRange!]; + } + return null; + } + + const continueSearchPredicate = createTimeBasedContinueBracketSearchPredicate(maxDuration); const lineCount = this.textModel.getLineCount(); const savedCounts = new Map<string, number[]>(); @@ -653,7 +729,7 @@ export class BracketPairsTextModelPart extends Disposable implements IBracketPai let languageId: string | null = null; let modeBrackets: RichEditBrackets | null = null; for (let lineNumber = position.lineNumber; lineNumber <= lineCount; lineNumber++) { - const lineTokens = this.textModel.getLineTokens(lineNumber); + const lineTokens = this.textModel.tokenization.getLineTokens(lineNumber); const tokenCount = lineTokens.getCount(); const lineText = this.textModel.getLineContent(lineNumber); @@ -725,7 +801,7 @@ export class BracketPairsTextModelPart extends Disposable implements IBracketPai return null; } - private _toFoundBracket(modeBrackets: RichEditBrackets, r: Range): IFoundBracket | null { + private _toFoundBracket(bracketConfig: LanguageBracketsConfiguration, r: Range): IFoundBracket | null { if (!r) { return null; } @@ -733,16 +809,14 @@ export class BracketPairsTextModelPart extends Disposable implements IBracketPai let text = this.textModel.getValueInRange(r); text = text.toLowerCase(); - const data = modeBrackets.textIsBracket[text]; - if (!data) { + const bracketInfo = bracketConfig.getBracketInfo(text); + if (!bracketInfo) { return null; } return { range: r, - open: data.open, - close: data.close, - isOpen: modeBrackets.textIsOpenBracket[text] + bracketInfo }; } } diff --git a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/ast.ts b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/ast.ts index 34e772bbdd8..e8dfd6f8f9f 100644 --- a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/ast.ts +++ b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/ast.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CursorColumns } from 'vs/editor/common/core/cursorColumns'; +import { BracketKind } from 'vs/editor/common/languages/supports/languageBracketsConfiguration'; import { ITextModel } from 'vs/editor/common/model'; import { Length, lengthAdd, lengthGetLineCount, lengthToObj, lengthZero } from './length'; import { SmallImmutableSet } from './smallImmutableSet'; @@ -626,11 +627,10 @@ export class TextAstNode extends ImmutableLeafAstNode { export class BracketAstNode extends ImmutableLeafAstNode { public static create( length: Length, - languageId: string, - text: string, + bracketInfo: BracketKind, bracketIds: SmallImmutableSet<OpeningBracketId> ): BracketAstNode { - const node = new BracketAstNode(length, languageId, text, bracketIds); + const node = new BracketAstNode(length, bracketInfo, bracketIds); return node; } @@ -644,8 +644,7 @@ export class BracketAstNode extends ImmutableLeafAstNode { private constructor( length: Length, - public readonly languageId: string, - public readonly text: string, + public readonly bracketInfo: BracketKind, /** * In case of a opening bracket, this is the id of the opening bracket. * In case of a closing bracket, this contains the ids of all opening brackets it can close. @@ -655,6 +654,14 @@ export class BracketAstNode extends ImmutableLeafAstNode { super(length); } + public get text() { + return this.bracketInfo.bracketText; + } + + public get languageId() { + return this.bracketInfo.languageId; + } + public canBeReused(_openedBracketIds: SmallImmutableSet<OpeningBracketId>) { // These nodes could be reused, // but not in a general way. diff --git a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/bracketPairsTree.ts b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/bracketPairsTree.ts index aa828744e7a..39c24d84072 100644 --- a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/bracketPairsTree.ts +++ b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/bracketPairsTree.ts @@ -7,17 +7,19 @@ import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { Range } from 'vs/editor/common/core/range'; import { ITextModel } from 'vs/editor/common/model'; -import { BracketInfo, BracketPairWithMinIndentationInfo } from 'vs/editor/common/textModelBracketPairs'; -import { BackgroundTokenizationState, TextModel } from 'vs/editor/common/model/textModel'; +import { BracketInfo, BracketPairWithMinIndentationInfo, IFoundBracket } from 'vs/editor/common/textModelBracketPairs'; +import { TextModel } from 'vs/editor/common/model/textModel'; import { IModelContentChangedEvent, IModelTokensChangedEvent } from 'vs/editor/common/textModelEvents'; import { ResolvedLanguageConfiguration } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { AstNode, AstNodeKind } from './ast'; import { TextEditInfo } from './beforeEditPositionMapper'; import { LanguageAgnosticBracketTokens } from './brackets'; -import { Length, lengthAdd, lengthGreaterThanEqual, lengthLessThanEqual, lengthOfString, lengthsToRange, lengthZero, positionToLength, toLength } from './length'; +import { Length, lengthAdd, lengthGreaterThanEqual, lengthLessThan, lengthLessThanEqual, lengthOfString, lengthsToRange, lengthZero, positionToLength, toLength } from './length'; import { parseDocument } from './parser'; import { DenseKeyProvider } from './smallImmutableSet'; import { FastTokenizer, TextBufferTokenizer } from './tokenizer'; +import { BackgroundTokenizationState } from 'vs/editor/common/tokenizationTextModelPart'; +import { Position } from 'vs/editor/common/core/position'; export class BracketPairsTree extends Disposable { private readonly didChangeEmitter = new Emitter<void>(); @@ -49,18 +51,18 @@ export class BracketPairsTree extends Disposable { ) { super(); - if (textModel.backgroundTokenizationState === BackgroundTokenizationState.Uninitialized) { + if (textModel.tokenization.backgroundTokenizationState === BackgroundTokenizationState.Uninitialized) { // There are no token information yet const brackets = this.brackets.getSingleLanguageBracketTokens(this.textModel.getLanguageId()); const tokenizer = new FastTokenizer(this.textModel.getValue(), brackets); this.initialAstWithoutTokens = parseDocument(tokenizer, [], undefined, true); this.astWithTokens = this.initialAstWithoutTokens; - } else if (textModel.backgroundTokenizationState === BackgroundTokenizationState.Completed) { + } else if (textModel.tokenization.backgroundTokenizationState === BackgroundTokenizationState.Completed) { // Skip the initial ast, as there is no flickering. // Directly create the tree with token information. this.initialAstWithoutTokens = undefined; this.astWithTokens = this.parseDocumentFromTextBuffer([], undefined, false); - } else if (textModel.backgroundTokenizationState === BackgroundTokenizationState.InProgress) { + } else if (textModel.tokenization.backgroundTokenizationState === BackgroundTokenizationState.InProgress) { this.initialAstWithoutTokens = this.parseDocumentFromTextBuffer([], undefined, true); this.astWithTokens = this.initialAstWithoutTokens; } @@ -69,7 +71,7 @@ export class BracketPairsTree extends Disposable { //#region TextModel events public handleDidChangeBackgroundTokenizationState(): void { - if (this.textModel.backgroundTokenizationState === BackgroundTokenizationState.Completed) { + if (this.textModel.tokenization.backgroundTokenizationState === BackgroundTokenizationState.Completed) { const wasUndefined = this.initialAstWithoutTokens === undefined; // Clear the initial tree as we can use the tree with token information now. this.initialAstWithoutTokens = undefined; @@ -144,6 +146,71 @@ export class BracketPairsTree extends Disposable { return result; } + + public getFirstBracketAfter(position: Position): IFoundBracket | null { + const node = this.initialAstWithoutTokens || this.astWithTokens!; + return getFirstBracketAfter(node, lengthZero, node.length, positionToLength(position)); + } + + public getFirstBracketBefore(position: Position): IFoundBracket | null { + const node = this.initialAstWithoutTokens || this.astWithTokens!; + return getFirstBracketBefore(node, lengthZero, node.length, positionToLength(position)); + } +} + +function getFirstBracketBefore(node: AstNode, nodeOffsetStart: Length, nodeOffsetEnd: Length, position: Length): IFoundBracket | null { + if (node.kind === AstNodeKind.List || node.kind === AstNodeKind.Pair) { + const lengths: { nodeOffsetStart: Length; nodeOffsetEnd: Length }[] = []; + for (const child of node.children) { + nodeOffsetEnd = lengthAdd(nodeOffsetStart, child.length); + lengths.push({ nodeOffsetStart, nodeOffsetEnd }); + nodeOffsetStart = nodeOffsetEnd; + } + for (let i = lengths.length - 1; i >= 0; i--) { + const { nodeOffsetStart, nodeOffsetEnd } = lengths[i]; + if (lengthLessThan(nodeOffsetStart, position)) { + const result = getFirstBracketBefore(node.children[i], nodeOffsetStart, nodeOffsetEnd, position); + if (result) { + return result; + } + } + } + return null; + } else if (node.kind === AstNodeKind.UnexpectedClosingBracket) { + return null; + } else if (node.kind === AstNodeKind.Bracket) { + const range = lengthsToRange(nodeOffsetStart, nodeOffsetEnd); + return { + bracketInfo: node.bracketInfo, + range + }; + } + return null; +} + +function getFirstBracketAfter(node: AstNode, nodeOffsetStart: Length, nodeOffsetEnd: Length, position: Length): IFoundBracket | null { + if (node.kind === AstNodeKind.List || node.kind === AstNodeKind.Pair) { + for (const child of node.children) { + nodeOffsetEnd = lengthAdd(nodeOffsetStart, child.length); + if (lengthLessThan(position, nodeOffsetEnd)) { + const result = getFirstBracketAfter(child, nodeOffsetStart, nodeOffsetEnd, position); + if (result) { + return result; + } + } + nodeOffsetStart = nodeOffsetEnd; + } + return null; + } else if (node.kind === AstNodeKind.UnexpectedClosingBracket) { + return null; + } else if (node.kind === AstNodeKind.Bracket) { + const range = lengthsToRange(nodeOffsetStart, nodeOffsetEnd); + return { + bracketInfo: node.bracketInfo, + range + }; + } + return null; } function collectBrackets( @@ -301,6 +368,7 @@ function collectBracketPairs( : undefined, level, levelPerBracket, + node, minIndentation ) ); diff --git a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/brackets.ts b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/brackets.ts index 458001931b1..280cb3b8deb 100644 --- a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/brackets.ts +++ b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/brackets.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { escapeRegExpCharacters } from 'vs/base/common/strings'; import { ResolvedLanguageConfiguration } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { BracketKind } from 'vs/editor/common/languages/supports/languageBracketsConfiguration'; import { BracketAstNode } from './ast'; import { toLength } from './length'; import { DenseKeyProvider, identityKeyProvider, SmallImmutableSet } from './smallImmutableSet'; @@ -11,50 +12,37 @@ import { OpeningBracketId, Token, TokenKind } from './tokenizer'; export class BracketTokens { static createFromLanguage(configuration: ResolvedLanguageConfiguration, denseKeyProvider: DenseKeyProvider<string>): BracketTokens { - function getId(languageId: string, openingText: string): OpeningBracketId { - return denseKeyProvider.getKey(`${languageId}:::${openingText}`); - } - - const brackets = configuration.characterPair.getColorizedBrackets(); - - const closingBrackets = new Map</* closingText */ string, { openingBrackets: SmallImmutableSet<OpeningBracketId>; first: OpeningBracketId }>(); - const openingBrackets = new Set</* openingText */ string>(); - - for (const [openingText, closingText] of brackets) { - openingBrackets.add(openingText); - - let info = closingBrackets.get(closingText); - const openingTextId = getId(configuration.languageId, openingText); - if (!info) { - info = { openingBrackets: SmallImmutableSet.getEmpty(), first: openingTextId }; - closingBrackets.set(closingText, info); - } - info.openingBrackets = info.openingBrackets.add(openingTextId, identityKeyProvider); + function getId(bracketInfo: BracketKind): OpeningBracketId { + return denseKeyProvider.getKey(`${bracketInfo.languageId}:::${bracketInfo.bracketText}`); } const map = new Map<string, Token>(); - - for (const [closingText, info] of closingBrackets) { - const length = toLength(0, closingText.length); - map.set(closingText, new Token( + for (const openingBracket of configuration.bracketsNew.openingBrackets) { + const length = toLength(0, openingBracket.bracketText.length); + const openingTextId = getId(openingBracket); + const bracketIds = SmallImmutableSet.getEmpty().add(openingTextId, identityKeyProvider); + map.set(openingBracket.bracketText, new Token( length, - TokenKind.ClosingBracket, - info.first, - info.openingBrackets, - BracketAstNode.create(length, configuration.languageId, closingText, info.openingBrackets) + TokenKind.OpeningBracket, + openingTextId, + bracketIds, + BracketAstNode.create(length, openingBracket, bracketIds) )); } - for (const openingText of openingBrackets) { - const length = toLength(0, openingText.length); - const openingTextId = getId(configuration.languageId, openingText); - const bracketIds = SmallImmutableSet.getEmpty().add(openingTextId, identityKeyProvider); - map.set(openingText, new Token( + for (const closingBracket of configuration.bracketsNew.closingBrackets) { + const length = toLength(0, closingBracket.bracketText.length); + let bracketIds = SmallImmutableSet.getEmpty(); + const closingBrackets = closingBracket.getClosedBrackets(); + for (const bracket of closingBrackets) { + bracketIds = bracketIds.add(getId(bracket), identityKeyProvider); + } + map.set(closingBracket.bracketText, new Token( length, - TokenKind.OpeningBracket, - openingTextId, + TokenKind.ClosingBracket, + getId(closingBrackets[0]), bracketIds, - BracketAstNode.create(length, configuration.languageId, openingText, bracketIds) + BracketAstNode.create(length, closingBracket, bracketIds) )); } @@ -85,14 +73,14 @@ export class BracketTokens { get regExpGlobal(): RegExp | null { if (!this.hasRegExp) { const regExpStr = this.getRegExpStr(); - this._regExpGlobal = regExpStr ? new RegExp(regExpStr, 'g') : null; + this._regExpGlobal = regExpStr ? new RegExp(regExpStr, 'gi') : null; this.hasRegExp = true; } return this._regExpGlobal; } getToken(value: string): Token | undefined { - return this.map.get(value); + return this.map.get(value.toLowerCase()); } findClosingTokenText(openingBracketIds: SmallImmutableSet<OpeningBracketId>): string | undefined { @@ -126,12 +114,8 @@ export class LanguageAgnosticBracketTokens { } public didLanguageChange(languageId: string): boolean { - const existing = this.languageIdToBracketTokens.get(languageId); - if (!existing) { - return false; - } - const newRegExpStr = BracketTokens.createFromLanguage(this.getLanguageConfiguration(languageId), this.denseKeyProvider).getRegExpStr(); - return existing.getRegExpStr() !== newRegExpStr; + // Report a change whenever the language configuration updates. + return this.languageIdToBracketTokens.has(languageId); } getSingleLanguageBracketTokens(languageId: string): BracketTokens { diff --git a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/tokenizer.ts b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/tokenizer.ts index 0c572140a2d..c65a57b0175 100644 --- a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/tokenizer.ts +++ b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/tokenizer.ts @@ -54,7 +54,10 @@ export interface ITokenizerSource { getValue(): string; getLineCount(): number; getLineLength(lineNumber: number): number; - getLineTokens(lineNumber: number): IViewLineTokens; + + tokenization: { + getLineTokens(lineNumber: number): IViewLineTokens; + }; } export class TextBufferTokenizer implements Tokenizer { @@ -166,7 +169,7 @@ class NonPeekableTextBufferTokenizer { } if (this.line === null) { - this.lineTokens = this.textModel.getLineTokens(this.lineIdx + 1); + this.lineTokens = this.textModel.tokenization.getLineTokens(this.lineIdx + 1); this.line = this.lineTokens.getLineContent(); this.lineTokenOffset = this.lineCharOffset === 0 ? 0 : this.lineTokens!.findTokenIndexAtOffset(this.lineCharOffset); } @@ -192,10 +195,11 @@ class NonPeekableTextBufferTokenizer { } const isOther = TokenMetadata.getTokenType(tokenMetadata) === StandardTokenType.Other; + const containsBracketType = TokenMetadata.containsBalancedBrackets(tokenMetadata); const endOffset = lineTokens.getEndOffset(this.lineTokenOffset); // Is there a bracket token next? Only consume text. - if (isOther && endOffset !== this.lineCharOffset) { + if (containsBracketType && isOther && endOffset !== this.lineCharOffset) { const languageId = lineTokens.getLanguageId(this.lineTokenOffset); const text = this.line.substring(this.lineCharOffset, endOffset); @@ -238,7 +242,7 @@ class NonPeekableTextBufferTokenizer { break; } this.lineIdx++; - this.lineTokens = this.textModel.getLineTokens(this.lineIdx + 1); + this.lineTokens = this.textModel.tokenization.getLineTokens(this.lineIdx + 1); this.lineTokenOffset = 0; this.line = this.lineTokens.getLineContent(); this.lineCharOffset = 0; @@ -276,7 +280,7 @@ export class FastTokenizer implements Tokenizer { constructor(private readonly text: string, brackets: BracketTokens) { const regExpStr = brackets.getRegExpStr(); - const regexp = regExpStr ? new RegExp(brackets.getRegExpStr() + '|\n', 'g') : null; + const regexp = regExpStr ? new RegExp(regExpStr + '|\n', 'gi') : null; const tokens: Token[] = []; diff --git a/src/vs/editor/common/model/bracketPairsTextModelPart/fixBrackets.ts b/src/vs/editor/common/model/bracketPairsTextModelPart/fixBrackets.ts index 866cd711a70..d9db755673f 100644 --- a/src/vs/editor/common/model/bracketPairsTextModelPart/fixBrackets.ts +++ b/src/vs/editor/common/model/bracketPairsTextModelPart/fixBrackets.ts @@ -76,7 +76,10 @@ class StaticTokenizerSource implements ITokenizerSource { getLineLength(lineNumber: number): number { return this.lines[lineNumber - 1].getLineContent().length; } - getLineTokens(lineNumber: number): IViewLineTokens { - return this.lines[lineNumber - 1]; - } + + tokenization = { + getLineTokens: (lineNumber: number): IViewLineTokens => { + return this.lines[lineNumber - 1]; + } + }; } diff --git a/src/vs/editor/common/model/guidesTextModelPart.ts b/src/vs/editor/common/model/guidesTextModelPart.ts index 488ee8e79d4..9e7937f7ad6 100644 --- a/src/vs/editor/common/model/guidesTextModelPart.ts +++ b/src/vs/editor/common/model/guidesTextModelPart.ts @@ -343,6 +343,10 @@ export class GuidesTextModelPart extends TextModelPart implements IGuidesTextMod ---- */ + if (!pair.closingBracketRange) { + continue; + } + const isActive = activeBracketPairRange && pair.range.equalsRange(activeBracketPairRange); if (!isActive && !options.includeInactive) { @@ -357,9 +361,7 @@ export class GuidesTextModelPart extends TextModelPart implements IGuidesTextMod const start = pair.openingBracketRange.getStartPosition(); - const end = - pair.closingBracketRange?.getStartPosition() ?? - pair.range.getEndPosition(); + const end = pair.closingBracketRange.getStartPosition(); const horizontalGuides = options.horizontalGuides === HorizontalGuidesState.Enabled || (options.horizontalGuides === HorizontalGuidesState.EnabledForActive && isActive); @@ -388,16 +390,16 @@ export class GuidesTextModelPart extends TextModelPart implements IGuidesTextMod const guideVisibleColumn = Math.min(startVisibleColumn, endVisibleColumn, pair.minVisibleColumnIndentation + 1); let renderHorizontalEndLineAtTheBottom = false; - if (pair.closingBracketRange) { - const firstNonWsIndex = strings.firstNonWhitespaceIndex( - this.textModel.getLineContent( - pair.closingBracketRange.startLineNumber - ) - ); - const hasTextBeforeClosingBracket = firstNonWsIndex < pair.closingBracketRange.startColumn - 1; - if (hasTextBeforeClosingBracket) { - renderHorizontalEndLineAtTheBottom = true; - } + + + const firstNonWsIndex = strings.firstNonWhitespaceIndex( + this.textModel.getLineContent( + pair.closingBracketRange.startLineNumber + ) + ); + const hasTextBeforeClosingBracket = firstNonWsIndex < pair.closingBracketRange.startColumn - 1; + if (hasTextBeforeClosingBracket) { + renderHorizontalEndLineAtTheBottom = true; } @@ -414,7 +416,6 @@ export class GuidesTextModelPart extends TextModelPart implements IGuidesTextMod className, null, l === start.lineNumber ? start.column : -1, - // TODO: Investigate if this is correct l === end.lineNumber ? end.column : -1 ) ); diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 78f17dd2669..8aa71a280d5 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -5,7 +5,6 @@ import { ArrayQueue, pushMany } from 'vs/base/common/arrays'; import { VSBuffer, VSBufferReadableStream } from 'vs/base/common/buffer'; -import { CharCode } from 'vs/base/common/charCode'; import { Color } from 'vs/base/common/color'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; @@ -15,39 +14,35 @@ import { listenStream } from 'vs/base/common/stream'; import * as strings from 'vs/base/common/strings'; import { Constants } from 'vs/base/common/uint'; import { URI } from 'vs/base/common/uri'; -import { LineTokens } from 'vs/editor/common/tokens/lineTokens'; +import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; +import { countEOL } from 'vs/editor/common/core/eolCounter'; +import { normalizeIndentation } from 'vs/editor/common/core/indentation'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; +import { TextChange } from 'vs/editor/common/core/textChange'; +import { EDITOR_MODEL_DEFAULTS } from 'vs/editor/common/core/textModelDefaults'; +import { IWordAtPosition } from 'vs/editor/common/core/wordHelper'; +import { FormattingOptions } from 'vs/editor/common/languages'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import * as model from 'vs/editor/common/model'; -import { IBracketPairsTextModelPart } from 'vs/editor/common/textModelBracketPairs'; import { BracketPairsTextModelPart } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsImpl'; import { ColorizedBracketPairsDecorationProvider } from 'vs/editor/common/model/bracketPairsTextModelPart/colorizedBracketPairsDecorationProvider'; import { EditStack } from 'vs/editor/common/model/editStack'; import { GuidesTextModelPart } from 'vs/editor/common/model/guidesTextModelPart'; -import { IGuidesTextModelPart } from 'vs/editor/common/textModelGuides'; import { guessIndentation } from 'vs/editor/common/model/indentationGuesser'; import { IntervalNode, IntervalTree, recomputeMaxEnd } from 'vs/editor/common/model/intervalTree'; import { PieceTreeTextBuffer } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer'; import { PieceTreeTextBufferBuilder } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder'; -import { TextChange } from 'vs/editor/common/core/textChange'; -import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent, IModelTokensChangedEvent, InternalModelContentChangeEvent, LineInjectedText, ModelInjectedTextChangedEvent, ModelRawChange, ModelRawContentChangedEvent, ModelRawEOLChanged, ModelRawFlush, ModelRawLineChanged, ModelRawLinesDeleted, ModelRawLinesInserted } from 'vs/editor/common/textModelEvents'; import { SearchParams, TextModelSearch } from 'vs/editor/common/model/textModelSearch'; -import { TextModelTokenization } from 'vs/editor/common/model/textModelTokens'; -import { countEOL } from 'vs/editor/common/core/eolCounter'; -import { ContiguousMultilineTokens } from 'vs/editor/common/tokens/contiguousMultilineTokens'; -import { SparseMultilineTokens } from 'vs/editor/common/tokens/sparseMultilineTokens'; -import { ContiguousTokensStore } from 'vs/editor/common/tokens/contiguousTokensStore'; -import { SparseTokensStore } from 'vs/editor/common/tokens/sparseTokensStore'; -import { getWordAtText, IWordAtPosition } from 'vs/editor/common/core/wordHelper'; -import { FormattingOptions, StandardTokenType } from 'vs/editor/common/languages'; -import { ILanguageConfigurationService, ResolvedLanguageConfiguration } from 'vs/editor/common/languages/languageConfigurationRegistry'; -import { ILanguageService } from 'vs/editor/common/languages/language'; +import { TokenizationTextModelPart } from 'vs/editor/common/model/tokenizationTextModelPart'; +import { IBracketPairsTextModelPart } from 'vs/editor/common/textModelBracketPairs'; +import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelOptionsChangedEvent, InternalModelContentChangeEvent, LineInjectedText, ModelInjectedTextChangedEvent, ModelRawChange, ModelRawContentChangedEvent, ModelRawEOLChanged, ModelRawFlush, ModelRawLineChanged, ModelRawLinesDeleted, ModelRawLinesInserted } from 'vs/editor/common/textModelEvents'; +import { IGuidesTextModelPart } from 'vs/editor/common/textModelGuides'; +import { ITokenizationTextModelPart } from 'vs/editor/common/tokenizationTextModelPart'; import { IColorTheme, ThemeColor } from 'vs/platform/theme/common/themeService'; import { IUndoRedoService, ResourceEditStackSnapshot } from 'vs/platform/undoRedo/common/undoRedo'; -import { EDITOR_MODEL_DEFAULTS } from 'vs/editor/common/core/textModelDefaults'; -import { normalizeIndentation } from 'vs/editor/common/core/indentation'; -import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; function createTextBufferBuilder() { return new PieceTreeTextBufferBuilder(); @@ -172,12 +167,6 @@ const enum StringOffsetValidationType { SurrogatePairs = 1, } -export const enum BackgroundTokenizationState { - Uninitialized = 0, - InProgress = 1, - Completed = 2, -} - export class TextModel extends Disposable implements model.ITextModel, IDecorationsTreesHost { private static readonly MODEL_SYNC_LIMIT = 50 * 1024 * 1024; // 50 MB @@ -227,14 +216,9 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati private readonly _onDidChangeDecorations: DidChangeDecorationsEmitter = this._register(new DidChangeDecorationsEmitter(affectedInjectedTextLines => this.handleBeforeFireDecorationsChangedEvent(affectedInjectedTextLines))); public readonly onDidChangeDecorations: Event<IModelDecorationsChangedEvent> = this._onDidChangeDecorations.event; - private readonly _onDidChangeLanguage: Emitter<IModelLanguageChangedEvent> = this._register(new Emitter<IModelLanguageChangedEvent>()); - public readonly onDidChangeLanguage: Event<IModelLanguageChangedEvent> = this._onDidChangeLanguage.event; - - private readonly _onDidChangeLanguageConfiguration: Emitter<IModelLanguageConfigurationChangedEvent> = this._register(new Emitter<IModelLanguageConfigurationChangedEvent>()); - public readonly onDidChangeLanguageConfiguration: Event<IModelLanguageConfigurationChangedEvent> = this._onDidChangeLanguageConfiguration.event; - - private readonly _onDidChangeTokens: Emitter<IModelTokensChangedEvent> = this._register(new Emitter<IModelTokensChangedEvent>()); - public readonly onDidChangeTokens: Event<IModelTokensChangedEvent> = this._onDidChangeTokens.event; + public get onDidChangeLanguage() { return this._tokenizationTextModelPart.onDidChangeLanguage; } + public get onDidChangeLanguageConfiguration() { return this._tokenizationTextModelPart.onDidChangeLanguageConfiguration; } + public get onDidChangeTokens() { return this._tokenizationTextModelPart.onDidChangeTokens; } private readonly _onDidChangeOptions: Emitter<IModelOptionsChangedEvent> = this._register(new Emitter<IModelOptionsChangedEvent>()); public readonly onDidChangeOptions: Event<IModelOptionsChangedEvent> = this._onDidChangeOptions.event; @@ -265,7 +249,8 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati private _options: model.TextModelResolvedOptions; private _isDisposed: boolean; - private _isDisposing: boolean; + private __isDisposing: boolean; + public _isDisposing(): boolean { return this.__isDisposing; } private _versionId: number; /** * Unlike, versionId, this can go down (via undo) or go to previous values (via redo) @@ -294,40 +279,15 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati private readonly _decorationProvider: ColorizedBracketPairsDecorationProvider; //#endregion - //#region Tokenization - private _languageId: string; - private readonly _languageRegistryListener: IDisposable; - private readonly _tokens: ContiguousTokensStore; - private readonly _semanticTokens: SparseTokensStore; - private readonly _tokenization: TextModelTokenization; - //#endregion + private readonly _tokenizationTextModelPart: TokenizationTextModelPart; + public get tokenization(): ITokenizationTextModelPart { return this._tokenizationTextModelPart; } - private readonly _bracketPairColorizer: BracketPairsTextModelPart; - public get bracketPairs(): IBracketPairsTextModelPart { return this._bracketPairColorizer; } + private readonly _bracketPairs: BracketPairsTextModelPart; + public get bracketPairs(): IBracketPairsTextModelPart { return this._bracketPairs; } private readonly _guidesTextModelPart: GuidesTextModelPart; public get guides(): IGuidesTextModelPart { return this._guidesTextModelPart; } - private _backgroundTokenizationState = BackgroundTokenizationState.Uninitialized; - public get backgroundTokenizationState(): BackgroundTokenizationState { - return this._backgroundTokenizationState; - } - private handleTokenizationProgress(completed: boolean) { - if (this._backgroundTokenizationState === BackgroundTokenizationState.Completed) { - // We already did a full tokenization and don't go back to progressing. - return; - } - const newState = completed ? BackgroundTokenizationState.Completed : BackgroundTokenizationState.InProgress; - if (this._backgroundTokenizationState !== newState) { - this._backgroundTokenizationState = newState; - this._bracketPairColorizer.handleDidChangeBackgroundTokenizationState(); - this._onBackgroundTokenizationStateChanged.fire(); - } - } - - private readonly _onBackgroundTokenizationStateChanged = this._register(new Emitter<void>()); - public readonly onBackgroundTokenizationStateChanged: Event<void> = this._onBackgroundTokenizationStateChanged.event; - constructor( source: string | model.ITextBufferFactory, languageId: string, @@ -356,6 +316,17 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati this._options = TextModel.resolveOptions(this._buffer, creationOptions); + this._bracketPairs = this._register(new BracketPairsTextModelPart(this, this._languageConfigurationService)); + this._guidesTextModelPart = this._register(new GuidesTextModelPart(this, this._languageConfigurationService)); + this._decorationProvider = this._register(new ColorizedBracketPairsDecorationProvider(this)); + this._tokenizationTextModelPart = new TokenizationTextModelPart( + this._languageService, + this._languageConfigurationService, + this, + this._bracketPairs, + languageId + ); + const bufferLineCount = this._buffer.getLineCount(); const bufferTextLength = this._buffer.getValueLengthInRange(new Range(1, 1, bufferLineCount, this._buffer.getLineLength(bufferLineCount) + 1), model.EndOfLinePreference.TextDefined); @@ -378,17 +349,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati this._initialUndoRedoSnapshot = null; this._isDisposed = false; - this._isDisposing = false; - - this._languageId = languageId; - - this._languageRegistryListener = this._languageConfigurationService.onDidChange( - e => { - if (e.affects(this._languageId)) { - this._onDidChangeLanguageConfiguration.fire({}); - } - } - ); + this.__isDisposing = false; this._instanceId = strings.singleLetterHash(MODEL_ID); this._lastDecorationId = 0; @@ -400,13 +361,6 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati this._isRedoing = false; this._trimAutoWhitespaceLines = null; - this._tokens = new ContiguousTokensStore(this._languageService.languageIdCodec); - this._semanticTokens = new SparseTokensStore(this._languageService.languageIdCodec); - this._tokenization = new TextModelTokenization(this, this._languageService.languageIdCodec); - - this._bracketPairColorizer = this._register(new BracketPairsTextModelPart(this, this._languageConfigurationService)); - this._guidesTextModelPart = this._register(new GuidesTextModelPart(this, this._languageConfigurationService)); - this._decorationProvider = this._register(new ColorizedBracketPairsDecorationProvider(this)); this._register(this._decorationProvider.onDidChange(() => { this._onDidChangeDecorations.beginDeferredEmit(); @@ -416,14 +370,13 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati } public override dispose(): void { - this._isDisposing = true; + this.__isDisposing = true; this._onWillDispose.fire(); - this._languageRegistryListener.dispose(); - this._tokenization.dispose(); + this._tokenizationTextModelPart.dispose(); this._isDisposed = true; super.dispose(); this._bufferDisposable.dispose(); - this._isDisposing = false; + this.__isDisposing = false; // Manually release reference to previous text buffer to avoid large leaks // in case someone leaks a TextModel reference const emptyDisposedTextBuffer = new PieceTreeTextBuffer([], '', '\n', false, false, true, true); @@ -436,14 +389,11 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati return ( this._onWillDispose.hasListeners() || this._onDidChangeDecorations.hasListeners() - || this._onDidChangeLanguage.hasListeners() - || this._onDidChangeLanguageConfiguration.hasListeners() - || this._onDidChangeTokens.hasListeners() + || this._tokenizationTextModelPart._hasListeners() || this._onDidChangeOptions.hasListeners() || this._onDidChangeAttached.hasListeners() || this._onDidChangeInjectedText.hasListeners() || this._eventEmitter.hasListeners() - || this._onBackgroundTokenizationStateChanged.hasListeners() ); } @@ -464,12 +414,12 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati } private _emitContentChangedEvent(rawChange: ModelRawContentChangedEvent, change: IModelContentChangedEvent): void { - if (this._isDisposing) { + if (this.__isDisposing) { // Do not confuse listeners by emitting any event after disposing return; } - this._bracketPairColorizer.handleDidChangeContent(change); - this._tokenization.handleDidChangeContent(change); + this._tokenizationTextModelPart.handleDidChangeContent(change); + this._bracketPairs.handleDidChangeContent(change); this._eventEmitter.fire(new InternalModelContentChangeEvent(rawChange, change)); } @@ -513,8 +463,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati this._increaseVersionId(); // Flush all tokens - this._tokens.flush(); - this._semanticTokens.flush(); + this._tokenizationTextModelPart.flush(); // Destroy all my decorations this._decorations = Object.create(null); @@ -600,7 +549,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati public onBeforeAttached(): void { this._attachedEditorCount++; if (this._attachedEditorCount === 1) { - this._tokenization.handleDidChangeAttached(); + this._tokenizationTextModelPart.handleDidChangeAttached(); this._onDidChangeAttached.fire(undefined); } } @@ -608,7 +557,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati public onBeforeDetached(): void { this._attachedEditorCount--; if (this._attachedEditorCount === 0) { - this._tokenization.handleDidChangeAttached(); + this._tokenizationTextModelPart.handleDidChangeAttached(); this._onDidChangeAttached.fire(undefined); } } @@ -697,7 +646,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati const e = this._options.createChangeEvent(newOpts); this._options = newOpts; - this._bracketPairColorizer.handleDidChangeOptions(e); + this._bracketPairs.handleDidChangeOptions(e); this._decorationProvider.handleDidChangeOptions(e); this._onDidChangeOptions.fire(e); } @@ -1463,8 +1412,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati for (let i = 0, len = contentChanges.length; i < len; i++) { const change = contentChanges[i]; const [eolCount, firstLineLength, lastLineLength] = countEOL(change.text); - this._tokens.acceptEdit(change.range, eolCount, firstLineLength); - this._semanticTokens.acceptEdit(change.range, eolCount, firstLineLength, lastLineLength, change.text.length > 0 ? change.text.charCodeAt(0) : CharCode.Null); + this._tokenizationTextModelPart.acceptEdit(change.range, change.text, eolCount, firstLineLength, lastLineLength); this._decorationsTree.acceptReplace(change.rangeOffset, change.rangeLength, change.text.length, change.forceMoveMarkers); } @@ -1944,270 +1892,28 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati //#region Tokenization - public setLineTokens(lineNumber: number, tokens: Uint32Array | ArrayBuffer | null): void { - if (lineNumber < 1 || lineNumber > this.getLineCount()) { - throw new Error('Illegal value for lineNumber'); - } - - this._tokens.setTokens(this._languageId, lineNumber - 1, this._buffer.getLineLength(lineNumber), tokens, false); - } - - public setTokens(tokens: ContiguousMultilineTokens[], backgroundTokenizationCompleted: boolean = false): void { - if (tokens.length !== 0) { - const ranges: { fromLineNumber: number; toLineNumber: number }[] = []; - - for (let i = 0, len = tokens.length; i < len; i++) { - const element = tokens[i]; - let minChangedLineNumber = 0; - let maxChangedLineNumber = 0; - let hasChange = false; - for (let lineNumber = element.startLineNumber; lineNumber <= element.endLineNumber; lineNumber++) { - if (hasChange) { - this._tokens.setTokens(this._languageId, lineNumber - 1, this._buffer.getLineLength(lineNumber), element.getLineTokens(lineNumber), false); - maxChangedLineNumber = lineNumber; - } else { - const lineHasChange = this._tokens.setTokens(this._languageId, lineNumber - 1, this._buffer.getLineLength(lineNumber), element.getLineTokens(lineNumber), true); - if (lineHasChange) { - hasChange = true; - minChangedLineNumber = lineNumber; - maxChangedLineNumber = lineNumber; - } - } - } - if (hasChange) { - ranges.push({ fromLineNumber: minChangedLineNumber, toLineNumber: maxChangedLineNumber }); - } - } - - if (ranges.length > 0) { - this._emitModelTokensChangedEvent({ - tokenizationSupportChanged: false, - semanticTokensApplied: false, - ranges: ranges - }); - } - } - this.handleTokenizationProgress(backgroundTokenizationCompleted); - } - - public setSemanticTokens(tokens: SparseMultilineTokens[] | null, isComplete: boolean): void { - this._semanticTokens.set(tokens, isComplete); - - this._emitModelTokensChangedEvent({ - tokenizationSupportChanged: false, - semanticTokensApplied: tokens !== null, - ranges: [{ fromLineNumber: 1, toLineNumber: this.getLineCount() }] - }); - } - - public hasCompleteSemanticTokens(): boolean { - return this._semanticTokens.isComplete(); - } - - public hasSomeSemanticTokens(): boolean { - return !this._semanticTokens.isEmpty(); - } - - public setPartialSemanticTokens(range: Range, tokens: SparseMultilineTokens[]): void { - if (this.hasCompleteSemanticTokens()) { - return; - } - const changedRange = this.validateRange(this._semanticTokens.setPartial(range, tokens)); - - this._emitModelTokensChangedEvent({ - tokenizationSupportChanged: false, - semanticTokensApplied: true, - ranges: [{ fromLineNumber: changedRange.startLineNumber, toLineNumber: changedRange.endLineNumber }] - }); - } - - public tokenizeViewport(startLineNumber: number, endLineNumber: number): void { - startLineNumber = Math.max(1, startLineNumber); - endLineNumber = Math.min(this._buffer.getLineCount(), endLineNumber); - this._tokenization.tokenizeViewport(startLineNumber, endLineNumber); - } - - public clearTokens(): void { - this._tokens.flush(); - this._emitModelTokensChangedEvent({ - tokenizationSupportChanged: true, - semanticTokensApplied: false, - ranges: [{ - fromLineNumber: 1, - toLineNumber: this._buffer.getLineCount() - }] - }); - } - - public clearSemanticTokens(): void { - this._semanticTokens.flush(); - - this._emitModelTokensChangedEvent({ - tokenizationSupportChanged: false, - semanticTokensApplied: false, - ranges: [{ fromLineNumber: 1, toLineNumber: this.getLineCount() }] - }); - } - - private _emitModelTokensChangedEvent(e: IModelTokensChangedEvent): void { - if (!this._isDisposing) { - this._bracketPairColorizer.handleDidChangeTokens(e); - this._onDidChangeTokens.fire(e); - } - } - - public resetTokenization(): void { - this._tokenization.reset(); - } - - public forceTokenization(lineNumber: number): void { - if (lineNumber < 1 || lineNumber > this.getLineCount()) { - throw new Error('Illegal value for lineNumber'); - } - - this._tokenization.forceTokenization(lineNumber); - } - - public isCheapToTokenize(lineNumber: number): boolean { - return this._tokenization.isCheapToTokenize(lineNumber); - } - - public tokenizeIfCheap(lineNumber: number): void { - if (this.isCheapToTokenize(lineNumber)) { - this.forceTokenization(lineNumber); - } - } - - public getLineTokens(lineNumber: number): LineTokens { - if (lineNumber < 1 || lineNumber > this.getLineCount()) { - throw new Error('Illegal value for lineNumber'); - } - - return this._getLineTokens(lineNumber); - } - - private _getLineTokens(lineNumber: number): LineTokens { - const lineText = this.getLineContent(lineNumber); - const syntacticTokens = this._tokens.getTokens(this._languageId, lineNumber - 1, lineText); - return this._semanticTokens.addSparseTokens(lineNumber, syntacticTokens); - } - + // TODO move them to the tokenization part. public getLanguageId(): string { - return this._languageId; + return this.tokenization.getLanguageId(); } public setMode(languageId: string): void { - if (this._languageId === languageId) { - // There's nothing to do - return; - } - - const e: IModelLanguageChangedEvent = { - oldLanguage: this._languageId, - newLanguage: languageId - }; - - this._languageId = languageId; - - this._bracketPairColorizer.handleDidChangeLanguage(e); - this._tokenization.handleDidChangeLanguage(e); - this._onDidChangeLanguage.fire(e); - this._onDidChangeLanguageConfiguration.fire({}); + this.tokenization.setLanguageId(languageId); } public getLanguageIdAtPosition(lineNumber: number, column: number): string { - const position = this.validatePosition(new Position(lineNumber, column)); - const lineTokens = this.getLineTokens(position.lineNumber); - return lineTokens.getLanguageId(lineTokens.findTokenIndexAtOffset(position.column - 1)); + return this.tokenization.getLanguageIdAtPosition(lineNumber, column); } - - public getTokenTypeIfInsertingCharacter(lineNumber: number, column: number, character: string): StandardTokenType { - const position = this.validatePosition(new Position(lineNumber, column)); - return this._tokenization.getTokenTypeIfInsertingCharacter(position, character); - } - - tokenizeLineWithEdit(position: IPosition, length: number, newText: string): LineTokens | null { - const validatedPosition = this.validatePosition(position); - return this._tokenization.tokenizeLineWithEdit(validatedPosition, length, newText); - } - - private getLanguageConfiguration(languageId: string): ResolvedLanguageConfiguration { - return this._languageConfigurationService.getLanguageConfiguration(languageId); - } - - // Having tokens allows implementing additional helper methods - - public getWordAtPosition(_position: IPosition): IWordAtPosition | null { - this._assertNotDisposed(); - const position = this.validatePosition(_position); - const lineContent = this.getLineContent(position.lineNumber); - const lineTokens = this._getLineTokens(position.lineNumber); - const tokenIndex = lineTokens.findTokenIndexAtOffset(position.column - 1); - - // (1). First try checking right biased word - const [rbStartOffset, rbEndOffset] = TextModel._findLanguageBoundaries(lineTokens, tokenIndex); - const rightBiasedWord = getWordAtText( - position.column, - this.getLanguageConfiguration(lineTokens.getLanguageId(tokenIndex)).getWordDefinition(), - lineContent.substring(rbStartOffset, rbEndOffset), - rbStartOffset - ); - // Make sure the result touches the original passed in position - if (rightBiasedWord && rightBiasedWord.startColumn <= _position.column && _position.column <= rightBiasedWord.endColumn) { - return rightBiasedWord; - } - - // (2). Else, if we were at a language boundary, check the left biased word - if (tokenIndex > 0 && rbStartOffset === position.column - 1) { - // edge case, where `position` sits between two tokens belonging to two different languages - const [lbStartOffset, lbEndOffset] = TextModel._findLanguageBoundaries(lineTokens, tokenIndex - 1); - const leftBiasedWord = getWordAtText( - position.column, - this.getLanguageConfiguration(lineTokens.getLanguageId(tokenIndex - 1)).getWordDefinition(), - lineContent.substring(lbStartOffset, lbEndOffset), - lbStartOffset - ); - // Make sure the result touches the original passed in position - if (leftBiasedWord && leftBiasedWord.startColumn <= _position.column && _position.column <= leftBiasedWord.endColumn) { - return leftBiasedWord; - } - } - - return null; + public setLineTokens(lineNumber: number, tokens: Uint32Array | ArrayBuffer | null): void { + this._tokenizationTextModelPart.setLineTokens(lineNumber, tokens); } - private static _findLanguageBoundaries(lineTokens: LineTokens, tokenIndex: number): [number, number] { - const languageId = lineTokens.getLanguageId(tokenIndex); - - // go left until a different language is hit - let startOffset = 0; - for (let i = tokenIndex; i >= 0 && lineTokens.getLanguageId(i) === languageId; i--) { - startOffset = lineTokens.getStartOffset(i); - } - - // go right until a different language is hit - let endOffset = lineTokens.getLineContent().length; - for (let i = tokenIndex, tokenCount = lineTokens.getCount(); i < tokenCount && lineTokens.getLanguageId(i) === languageId; i++) { - endOffset = lineTokens.getEndOffset(i); - } - - return [startOffset, endOffset]; + public getWordAtPosition(position: IPosition): IWordAtPosition | null { + return this._tokenizationTextModelPart.getWordAtPosition(position); } public getWordUntilPosition(position: IPosition): IWordAtPosition { - const wordAtPosition = this.getWordAtPosition(position); - if (!wordAtPosition) { - return { - word: '', - startColumn: position.column, - endColumn: position.column - }; - } - return { - word: wordAtPosition.word.substr(0, position.column - wordAtPosition.startColumn), - startColumn: wordAtPosition.startColumn, - endColumn: position.column - }; + return this._tokenizationTextModelPart.getWordUntilPosition(position); } //#endregion diff --git a/src/vs/editor/common/model/textModelPart.ts b/src/vs/editor/common/model/textModelPart.ts index 7a37fda1f3a..ef061c2ba9c 100644 --- a/src/vs/editor/common/model/textModelPart.ts +++ b/src/vs/editor/common/model/textModelPart.ts @@ -3,12 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; -export class TextModelPart implements IDisposable { +export class TextModelPart extends Disposable { private _isDisposed = false; - public dispose(): void { + public override dispose(): void { + super.dispose(); this._isDisposed = true; } protected assertNotDisposed(): void { diff --git a/src/vs/editor/common/model/textModelTokens.ts b/src/vs/editor/common/model/textModelTokens.ts index 89accddef32..a0aaf377690 100644 --- a/src/vs/editor/common/model/textModelTokens.ts +++ b/src/vs/editor/common/model/textModelTokens.ts @@ -18,6 +18,7 @@ import { ContiguousMultilineTokensBuilder } from 'vs/editor/common/tokens/contig import { runWhenIdle, IdleDeadline } from 'vs/base/common/async'; import { setTimeout0 } from 'vs/base/common/platform'; import { IModelContentChangedEvent, IModelLanguageChangedEvent } from 'vs/editor/common/textModelEvents'; +import { TokenizationTextModelPart } from 'vs/editor/common/model/tokenizationTextModelPart'; const enum Constants { CHEAP_TOKENIZATION_LENGTH_LIMIT = 2048 @@ -166,6 +167,7 @@ export class TextModelTokenization extends Disposable { constructor( private readonly _textModel: TextModel, + private readonly _tokenizationPart: TokenizationTextModelPart, private readonly _languageIdCodec: ILanguageIdCodec ) { super(); @@ -179,7 +181,7 @@ export class TextModelTokenization extends Disposable { } this._resetTokenizationState(); - this._textModel.clearTokens(); + this._tokenizationPart.clearTokens(); })); this._resetTokenizationState(); @@ -214,13 +216,13 @@ export class TextModelTokenization extends Disposable { public handleDidChangeLanguage(e: IModelLanguageChangedEvent): void { this._resetTokenizationState(); - this._textModel.clearTokens(); + this._tokenizationPart.clearTokens(); } //#endregion private _resetTokenizationState(): void { - const [tokenizationSupport, initialState] = initializeTokenization(this._textModel); + const [tokenizationSupport, initialState] = initializeTokenization(this._textModel, this._tokenizationPart); if (tokenizationSupport && initialState) { this._tokenizationStateStore = new TokenizationStateStore(tokenizationSupport, initialState); } else { @@ -294,24 +296,24 @@ export class TextModelTokenization extends Disposable { } } while (this._hasLinesToTokenize()); - this._textModel.setTokens(builder.finalize(), this._isTokenizationComplete()); + this._tokenizationPart.setTokens(builder.finalize(), this._isTokenizationComplete()); } public tokenizeViewport(startLineNumber: number, endLineNumber: number): void { const builder = new ContiguousMultilineTokensBuilder(); this._tokenizeViewport(builder, startLineNumber, endLineNumber); - this._textModel.setTokens(builder.finalize(), this._isTokenizationComplete()); + this._tokenizationPart.setTokens(builder.finalize(), this._isTokenizationComplete()); } public reset(): void { this._resetTokenizationState(); - this._textModel.clearTokens(); + this._tokenizationPart.clearTokens(); } public forceTokenization(lineNumber: number): void { const builder = new ContiguousMultilineTokensBuilder(); this._updateTokensUntilLine(builder, lineNumber); - this._textModel.setTokens(builder.finalize(), this._isTokenizationComplete()); + this._tokenizationPart.setTokens(builder.finalize(), this._isTokenizationComplete()); } public getTokenTypeIfInsertingCharacter(position: Position, character: string): StandardTokenType { @@ -499,11 +501,11 @@ export class TextModelTokenization extends Disposable { } } -function initializeTokenization(textModel: TextModel): [ITokenizationSupport, IState] | [null, null] { +function initializeTokenization(textModel: TextModel, tokenizationPart: TokenizationTextModelPart): [ITokenizationSupport, IState] | [null, null] { if (textModel.isTooLargeForTokenization()) { return [null, null]; } - const tokenizationSupport = TokenizationRegistry.get(textModel.getLanguageId()); + const tokenizationSupport = TokenizationRegistry.get(tokenizationPart.getLanguageId()); if (!tokenizationSupport) { return [null, null]; } diff --git a/src/vs/editor/common/model/tokenizationTextModelPart.ts b/src/vs/editor/common/model/tokenizationTextModelPart.ts new file mode 100644 index 00000000000..df1000db4cd --- /dev/null +++ b/src/vs/editor/common/model/tokenizationTextModelPart.ts @@ -0,0 +1,506 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from 'vs/base/common/event'; +import { CharCode } from 'vs/base/common/charCode'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { IPosition, Position } from 'vs/editor/common/core/position'; +import { IRange, Range } from 'vs/editor/common/core/range'; +import { getWordAtText, IWordAtPosition } from 'vs/editor/common/core/wordHelper'; +import { StandardTokenType } from 'vs/editor/common/languages'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { ILanguageConfigurationService, ResolvedLanguageConfiguration } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { TextModel } from 'vs/editor/common/model/textModel'; +import { TextModelPart } from 'vs/editor/common/model/textModelPart'; +import { TextModelTokenization } from 'vs/editor/common/model/textModelTokens'; +import { IModelContentChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelTokensChangedEvent } from 'vs/editor/common/textModelEvents'; +import { ContiguousMultilineTokens } from 'vs/editor/common/tokens/contiguousMultilineTokens'; +import { ContiguousTokensStore } from 'vs/editor/common/tokens/contiguousTokensStore'; +import { LineTokens } from 'vs/editor/common/tokens/lineTokens'; +import { SparseMultilineTokens } from 'vs/editor/common/tokens/sparseMultilineTokens'; +import { SparseTokensStore } from 'vs/editor/common/tokens/sparseTokensStore'; +import { BracketPairsTextModelPart } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsImpl'; +import { BackgroundTokenizationState, ITokenizationTextModelPart } from 'vs/editor/common/tokenizationTextModelPart'; + +export class TokenizationTextModelPart extends TextModelPart implements ITokenizationTextModelPart { + private readonly _onDidChangeLanguage: Emitter<IModelLanguageChangedEvent> = this._register(new Emitter<IModelLanguageChangedEvent>()); + public readonly onDidChangeLanguage: Event<IModelLanguageChangedEvent> = this._onDidChangeLanguage.event; + + private readonly _onDidChangeLanguageConfiguration: Emitter<IModelLanguageConfigurationChangedEvent> = this._register(new Emitter<IModelLanguageConfigurationChangedEvent>()); + public readonly onDidChangeLanguageConfiguration: Event<IModelLanguageConfigurationChangedEvent> = this._onDidChangeLanguageConfiguration.event; + + private readonly _onDidChangeTokens: Emitter<IModelTokensChangedEvent> = this._register(new Emitter<IModelTokensChangedEvent>()); + public readonly onDidChangeTokens: Event<IModelTokensChangedEvent> = this._onDidChangeTokens.event; + + private readonly _languageRegistryListener: IDisposable; + private readonly _tokens: ContiguousTokensStore; + private readonly _semanticTokens: SparseTokensStore; + private readonly _tokenization: TextModelTokenization; + + constructor( + private readonly _languageService: ILanguageService, + private readonly _languageConfigurationService: ILanguageConfigurationService, + private readonly _textModel: TextModel, + private readonly bracketPairsTextModelPart: BracketPairsTextModelPart, + private _languageId: string, + ) { + super(); + + this._tokens = new ContiguousTokensStore( + this._languageService.languageIdCodec + ); + this._semanticTokens = new SparseTokensStore( + this._languageService.languageIdCodec + ); + this._tokenization = new TextModelTokenization( + _textModel, + this, + this._languageService.languageIdCodec + ); + + this._languageRegistryListener = this._languageConfigurationService.onDidChange( + e => { + if (e.affects(this._languageId)) { + this._onDidChangeLanguageConfiguration.fire({}); + } + } + ); + } + + _hasListeners(): boolean { + return ( + this._onDidChangeLanguage.hasListeners() + || this._onDidChangeLanguageConfiguration.hasListeners() + || this._onDidChangeTokens.hasListeners() + || this._onBackgroundTokenizationStateChanged.hasListeners() + ); + } + + public acceptEdit( + range: IRange, + text: string, + eolCount: number, + firstLineLength: number, + lastLineLength: number + ): void { + this._tokens.acceptEdit(range, eolCount, firstLineLength); + this._semanticTokens.acceptEdit( + range, + eolCount, + firstLineLength, + lastLineLength, + text.length > 0 ? text.charCodeAt(0) : CharCode.Null + ); + } + + public handleDidChangeAttached(): void { + this._tokenization.handleDidChangeAttached(); + } + + public flush(): void { + this._tokens.flush(); + this._semanticTokens.flush(); + } + + public handleDidChangeContent(change: IModelContentChangedEvent): void { + this._tokenization.handleDidChangeContent(change); + } + + public override dispose(): void { + this._languageRegistryListener.dispose(); + this._tokenization.dispose(); + super.dispose(); + } + + private _backgroundTokenizationState = BackgroundTokenizationState.Uninitialized; + public get backgroundTokenizationState(): BackgroundTokenizationState { + return this._backgroundTokenizationState; + } + private handleTokenizationProgress(completed: boolean) { + if (this._backgroundTokenizationState === BackgroundTokenizationState.Completed) { + // We already did a full tokenization and don't go back to progressing. + return; + } + const newState = completed ? BackgroundTokenizationState.Completed : BackgroundTokenizationState.InProgress; + if (this._backgroundTokenizationState !== newState) { + this._backgroundTokenizationState = newState; + this.bracketPairsTextModelPart.handleDidChangeBackgroundTokenizationState(); + this._onBackgroundTokenizationStateChanged.fire(); + } + } + + private readonly _onBackgroundTokenizationStateChanged = this._register(new Emitter<void>()); + public readonly onBackgroundTokenizationStateChanged: Event<void> = this._onBackgroundTokenizationStateChanged.event; + + public setLineTokens( + lineNumber: number, + tokens: Uint32Array | ArrayBuffer | null + ): void { + if (lineNumber < 1 || lineNumber > this._textModel.getLineCount()) { + throw new Error('Illegal value for lineNumber'); + } + + this._tokens.setTokens( + this._languageId, + lineNumber - 1, + this._textModel.getLineLength(lineNumber), + tokens, + false + ); + } + + public setTokens( + tokens: ContiguousMultilineTokens[], + backgroundTokenizationCompleted: boolean = false + ): void { + if (tokens.length !== 0) { + const ranges: { fromLineNumber: number; toLineNumber: number }[] = []; + + for (let i = 0, len = tokens.length; i < len; i++) { + const element = tokens[i]; + let minChangedLineNumber = 0; + let maxChangedLineNumber = 0; + let hasChange = false; + for ( + let lineNumber = element.startLineNumber; + lineNumber <= element.endLineNumber; + lineNumber++ + ) { + if (hasChange) { + this._tokens.setTokens( + this._languageId, + lineNumber - 1, + this._textModel.getLineLength(lineNumber), + element.getLineTokens(lineNumber), + false + ); + maxChangedLineNumber = lineNumber; + } else { + const lineHasChange = this._tokens.setTokens( + this._languageId, + lineNumber - 1, + this._textModel.getLineLength(lineNumber), + element.getLineTokens(lineNumber), + true + ); + if (lineHasChange) { + hasChange = true; + minChangedLineNumber = lineNumber; + maxChangedLineNumber = lineNumber; + } + } + } + if (hasChange) { + ranges.push({ + fromLineNumber: minChangedLineNumber, + toLineNumber: maxChangedLineNumber, + }); + } + } + + if (ranges.length > 0) { + this._emitModelTokensChangedEvent({ + tokenizationSupportChanged: false, + semanticTokensApplied: false, + ranges: ranges, + }); + } + } + this.handleTokenizationProgress(backgroundTokenizationCompleted); + } + + public setSemanticTokens( + tokens: SparseMultilineTokens[] | null, + isComplete: boolean + ): void { + this._semanticTokens.set(tokens, isComplete); + + this._emitModelTokensChangedEvent({ + tokenizationSupportChanged: false, + semanticTokensApplied: tokens !== null, + ranges: [{ fromLineNumber: 1, toLineNumber: this._textModel.getLineCount() }], + }); + } + + public hasCompleteSemanticTokens(): boolean { + return this._semanticTokens.isComplete(); + } + + public hasSomeSemanticTokens(): boolean { + return !this._semanticTokens.isEmpty(); + } + + public setPartialSemanticTokens( + range: Range, + tokens: SparseMultilineTokens[] + ): void { + if (this.hasCompleteSemanticTokens()) { + return; + } + const changedRange = this._textModel.validateRange( + this._semanticTokens.setPartial(range, tokens) + ); + + this._emitModelTokensChangedEvent({ + tokenizationSupportChanged: false, + semanticTokensApplied: true, + ranges: [ + { + fromLineNumber: changedRange.startLineNumber, + toLineNumber: changedRange.endLineNumber, + }, + ], + }); + } + + public tokenizeViewport( + startLineNumber: number, + endLineNumber: number + ): void { + startLineNumber = Math.max(1, startLineNumber); + endLineNumber = Math.min(this._textModel.getLineCount(), endLineNumber); + this._tokenization.tokenizeViewport(startLineNumber, endLineNumber); + } + + public clearTokens(): void { + this._tokens.flush(); + this._emitModelTokensChangedEvent({ + tokenizationSupportChanged: true, + semanticTokensApplied: false, + ranges: [ + { + fromLineNumber: 1, + toLineNumber: this._textModel.getLineCount(), + }, + ], + }); + } + + public clearSemanticTokens(): void { + this._semanticTokens.flush(); + + this._emitModelTokensChangedEvent({ + tokenizationSupportChanged: false, + semanticTokensApplied: false, + ranges: [{ fromLineNumber: 1, toLineNumber: this._textModel.getLineCount() }], + }); + } + + private _emitModelTokensChangedEvent(e: IModelTokensChangedEvent): void { + if (!this._textModel._isDisposing()) { + this.bracketPairsTextModelPart.handleDidChangeTokens(e); + this._onDidChangeTokens.fire(e); + } + } + + public resetTokenization(): void { + this._tokenization.reset(); + } + + public forceTokenization(lineNumber: number): void { + if (lineNumber < 1 || lineNumber > this._textModel.getLineCount()) { + throw new Error('Illegal value for lineNumber'); + } + + this._tokenization.forceTokenization(lineNumber); + } + + public isCheapToTokenize(lineNumber: number): boolean { + return this._tokenization.isCheapToTokenize(lineNumber); + } + + public tokenizeIfCheap(lineNumber: number): void { + if (this.isCheapToTokenize(lineNumber)) { + this.forceTokenization(lineNumber); + } + } + + public getLineTokens(lineNumber: number): LineTokens { + if (lineNumber < 1 || lineNumber > this._textModel.getLineCount()) { + throw new Error('Illegal value for lineNumber'); + } + + return this._getLineTokens(lineNumber); + } + + private _getLineTokens(lineNumber: number): LineTokens { + const lineText = this._textModel.getLineContent(lineNumber); + const syntacticTokens = this._tokens.getTokens( + this._languageId, + lineNumber - 1, + lineText + ); + return this._semanticTokens.addSparseTokens(lineNumber, syntacticTokens); + } + + public getTokenTypeIfInsertingCharacter( + lineNumber: number, + column: number, + character: string + ): StandardTokenType { + const position = this._textModel.validatePosition(new Position(lineNumber, column)); + return this._tokenization.getTokenTypeIfInsertingCharacter( + position, + character + ); + } + + public tokenizeLineWithEdit( + position: IPosition, + length: number, + newText: string + ): LineTokens | null { + const validatedPosition = this._textModel.validatePosition(position); + return this._tokenization.tokenizeLineWithEdit( + validatedPosition, + length, + newText + ); + } + + private getLanguageConfiguration( + languageId: string + ): ResolvedLanguageConfiguration { + return this._languageConfigurationService.getLanguageConfiguration( + languageId + ); + } + + // Having tokens allows implementing additional helper methods + + public getWordAtPosition(_position: IPosition): IWordAtPosition | null { + this.assertNotDisposed(); + const position = this._textModel.validatePosition(_position); + const lineContent = this._textModel.getLineContent(position.lineNumber); + const lineTokens = this._getLineTokens(position.lineNumber); + const tokenIndex = lineTokens.findTokenIndexAtOffset(position.column - 1); + + // (1). First try checking right biased word + const [rbStartOffset, rbEndOffset] = TokenizationTextModelPart._findLanguageBoundaries( + lineTokens, + tokenIndex + ); + const rightBiasedWord = getWordAtText( + position.column, + this.getLanguageConfiguration( + lineTokens.getLanguageId(tokenIndex) + ).getWordDefinition(), + lineContent.substring(rbStartOffset, rbEndOffset), + rbStartOffset + ); + // Make sure the result touches the original passed in position + if ( + rightBiasedWord && + rightBiasedWord.startColumn <= _position.column && + _position.column <= rightBiasedWord.endColumn + ) { + return rightBiasedWord; + } + + // (2). Else, if we were at a language boundary, check the left biased word + if (tokenIndex > 0 && rbStartOffset === position.column - 1) { + // edge case, where `position` sits between two tokens belonging to two different languages + const [lbStartOffset, lbEndOffset] = TokenizationTextModelPart._findLanguageBoundaries( + lineTokens, + tokenIndex - 1 + ); + const leftBiasedWord = getWordAtText( + position.column, + this.getLanguageConfiguration( + lineTokens.getLanguageId(tokenIndex - 1) + ).getWordDefinition(), + lineContent.substring(lbStartOffset, lbEndOffset), + lbStartOffset + ); + // Make sure the result touches the original passed in position + if ( + leftBiasedWord && + leftBiasedWord.startColumn <= _position.column && + _position.column <= leftBiasedWord.endColumn + ) { + return leftBiasedWord; + } + } + + return null; + } + + private static _findLanguageBoundaries( + lineTokens: LineTokens, + tokenIndex: number + ): [number, number] { + const languageId = lineTokens.getLanguageId(tokenIndex); + + // go left until a different language is hit + let startOffset = 0; + for ( + let i = tokenIndex; + i >= 0 && lineTokens.getLanguageId(i) === languageId; + i-- + ) { + startOffset = lineTokens.getStartOffset(i); + } + + // go right until a different language is hit + let endOffset = lineTokens.getLineContent().length; + for ( + let i = tokenIndex, tokenCount = lineTokens.getCount(); + i < tokenCount && lineTokens.getLanguageId(i) === languageId; + i++ + ) { + endOffset = lineTokens.getEndOffset(i); + } + + return [startOffset, endOffset]; + } + + public getWordUntilPosition(position: IPosition): IWordAtPosition { + const wordAtPosition = this.getWordAtPosition(position); + if (!wordAtPosition) { + return { + word: '', + startColumn: position.column, + endColumn: position.column, + }; + } + return { + word: wordAtPosition.word.substr( + 0, + position.column - wordAtPosition.startColumn + ), + startColumn: wordAtPosition.startColumn, + endColumn: position.column, + }; + } + + public getLanguageId(): string { + return this._languageId; + } + + public getLanguageIdAtPosition(lineNumber: number, column: number): string { + const position = this._textModel.validatePosition(new Position(lineNumber, column)); + const lineTokens = this.getLineTokens(position.lineNumber); + return lineTokens.getLanguageId(lineTokens.findTokenIndexAtOffset(position.column - 1)); + } + + public setLanguageId(languageId: string): void { + if (this._languageId === languageId) { + // There's nothing to do + return; + } + + const e: IModelLanguageChangedEvent = { + oldLanguage: this._languageId, + newLanguage: languageId + }; + + this._languageId = languageId; + + this.bracketPairsTextModelPart.handleDidChangeLanguage(e); + this._tokenization.handleDidChangeLanguage(e); + this._onDidChangeLanguage.fire(e); + this._onDidChangeLanguageConfiguration.fire({}); + } +} diff --git a/src/vs/editor/common/services/languagesAssociations.ts b/src/vs/editor/common/services/languagesAssociations.ts index 4912ff8c7cf..ac7eb2b567b 100644 --- a/src/vs/editor/common/services/languagesAssociations.ts +++ b/src/vs/editor/common/services/languagesAssociations.ts @@ -152,6 +152,10 @@ function getAssociations(resource: URI | null, firstLine?: string): IdAndMime[] path = metadata.get(DataUri.META_DATA_LABEL); break; } + case Schemas.vscodeNotebookCell: + // File path not relevant for language detection of cell + path = undefined; + break; default: path = resource.path; } diff --git a/src/vs/editor/common/services/modelService.ts b/src/vs/editor/common/services/modelService.ts index 6f6e79ee9a5..cdcd12ecfa2 100644 --- a/src/vs/editor/common/services/modelService.ts +++ b/src/vs/editor/common/services/modelService.ts @@ -850,7 +850,7 @@ export class ModelSemanticColoring extends Disposable { // there is no provider if (this._currentDocumentResponse) { // there are semantic tokens set - this._model.setSemanticTokens(null, false); + this._model.tokenization.setSemanticTokens(null, false); } return; } @@ -925,11 +925,11 @@ export class ModelSemanticColoring extends Disposable { return; } if (!provider || !styling) { - this._model.setSemanticTokens(null, false); + this._model.tokenization.setSemanticTokens(null, false); return; } if (!tokens) { - this._model.setSemanticTokens(null, true); + this._model.tokenization.setSemanticTokens(null, true); rescheduleIfNeeded(); return; } @@ -937,7 +937,7 @@ export class ModelSemanticColoring extends Disposable { if (isSemanticTokensEdits(tokens)) { if (!currentResponse) { // not possible! - this._model.setSemanticTokens(null, true); + this._model.tokenization.setSemanticTokens(null, true); return; } if (tokens.edits.length === 0) { @@ -1006,9 +1006,9 @@ export class ModelSemanticColoring extends Disposable { } } - this._model.setSemanticTokens(result, true); + this._model.tokenization.setSemanticTokens(result, true); } else { - this._model.setSemanticTokens(null, true); + this._model.tokenization.setSemanticTokens(null, true); } rescheduleIfNeeded(); diff --git a/src/vs/editor/common/textModelBracketPairs.ts b/src/vs/editor/common/textModelBracketPairs.ts index a484153d169..633b72fdd40 100644 --- a/src/vs/editor/common/textModelBracketPairs.ts +++ b/src/vs/editor/common/textModelBracketPairs.ts @@ -6,6 +6,8 @@ import { Event } from 'vs/base/common/event'; import { IPosition } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; +import { ClosingBracketKind, OpeningBracketKind } from 'vs/editor/common/languages/supports/languageBracketsConfiguration'; +import { PairAstNode } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/ast'; export interface IBracketPairsTextModelPart { /** @@ -65,9 +67,7 @@ export interface IBracketPairsTextModelPart { export interface IFoundBracket { range: Range; - open: string[]; - close: string[]; - isOpen: boolean; + bracketInfo: OpeningBracketKind | ClosingBracketKind; } export class BracketInfo { @@ -88,7 +88,18 @@ export class BracketPairInfo { /** 0-based */ public readonly nestingLevel: number, public readonly nestingLevelOfEqualBracketType: number, - ) { } + private readonly bracketPairNode: PairAstNode, + + ) { + } + + public get openingBracketInfo(): OpeningBracketKind { + return this.bracketPairNode.openingBracket.bracketInfo as OpeningBracketKind; + } + + public get closingBracketInfo(): ClosingBracketKind | undefined { + return this.bracketPairNode.closingBracket?.bracketInfo as ClosingBracketKind | undefined; + } } export class BracketPairWithMinIndentationInfo extends BracketPairInfo { @@ -101,11 +112,12 @@ export class BracketPairWithMinIndentationInfo extends BracketPairInfo { */ nestingLevel: number, nestingLevelOfEqualBracketType: number, + bracketPairNode: PairAstNode, /** * -1 if not requested, otherwise the size of the minimum indentation in the bracket pair in terms of visible columns. */ public readonly minVisibleColumnIndentation: number, ) { - super(range, openingBracketRange, closingBracketRange, nestingLevel, nestingLevelOfEqualBracketType); + super(range, openingBracketRange, closingBracketRange, nestingLevel, nestingLevelOfEqualBracketType, bracketPairNode); } } diff --git a/src/vs/editor/common/textModelGuides.ts b/src/vs/editor/common/textModelGuides.ts index 7b63ee6b4d1..01789ed6b0c 100644 --- a/src/vs/editor/common/textModelGuides.ts +++ b/src/vs/editor/common/textModelGuides.ts @@ -52,8 +52,11 @@ export class IndentGuide { * It starts at visibleColumn and continues until endColumn. */ public readonly horizontalLine: IndentGuideHorizontalLine | null, + /** + * If set (!= -1), only show this guide for wrapped lines that don't contain this model column, but are after it. + */ public readonly forWrappedLinesAfterColumn: number | -1, - public readonly forWrappedLinesBeforeColumn: number | -1 + public readonly forWrappedLinesBeforeOrAtColumn: number | -1 ) { if ((visibleColumn !== -1) === (column !== -1)) { throw new Error(); diff --git a/src/vs/editor/common/tokenizationTextModelPart.ts b/src/vs/editor/common/tokenizationTextModelPart.ts new file mode 100644 index 00000000000..b68ce485850 --- /dev/null +++ b/src/vs/editor/common/tokenizationTextModelPart.ts @@ -0,0 +1,108 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event } from 'vs/base/common/event'; +import { IPosition } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; +import { StandardTokenType } from 'vs/editor/common/languages'; +import { ContiguousMultilineTokens } from 'vs/editor/common/tokens/contiguousMultilineTokens'; +import { LineTokens } from 'vs/editor/common/tokens/lineTokens'; +import { SparseMultilineTokens } from 'vs/editor/common/tokens/sparseMultilineTokens'; + +/** + * Provides tokenization related functionality of the text model. +*/ +export interface ITokenizationTextModelPart { + /** + * @internal + */ + setTokens(tokens: ContiguousMultilineTokens[]): void; + + /** + * Replaces all semantic tokens with the provided `tokens`. + * @internal + */ + setSemanticTokens(tokens: SparseMultilineTokens[] | null, isComplete: boolean): void; + + /** + * Merges the provided semantic tokens into existing semantic tokens. + * @internal + */ + setPartialSemanticTokens(range: Range, tokens: SparseMultilineTokens[] | null): void; + + /** + * @internal + */ + hasCompleteSemanticTokens(): boolean; + + /** + * @internal + */ + hasSomeSemanticTokens(): boolean; + + /** + * Flush all tokenization state. + * @internal + */ + resetTokenization(): void; + + /** + * Force tokenization information for `lineNumber` to be accurate. + * @internal + */ + forceTokenization(lineNumber: number): void; + + /** + * If it is cheap, force tokenization information for `lineNumber` to be accurate. + * This is based on a heuristic. + * @internal + */ + tokenizeIfCheap(lineNumber: number): void; + + /** + * Check if calling `forceTokenization` for this `lineNumber` will be cheap (time-wise). + * This is based on a heuristic. + * @internal + */ + isCheapToTokenize(lineNumber: number): boolean; + + /** + * Get the tokens for the line `lineNumber`. + * The tokens might be inaccurate. Use `forceTokenization` to ensure accurate tokens. + * @internal + */ + getLineTokens(lineNumber: number): LineTokens; + + /** + * Returns the standard token type for a character if the character were to be inserted at + * the given position. If the result cannot be accurate, it returns null. + * @internal + */ + getTokenTypeIfInsertingCharacter(lineNumber: number, column: number, character: string): StandardTokenType; + + /** + * @internal + */ + tokenizeLineWithEdit(position: IPosition, length: number, newText: string): LineTokens | null; + + /** + * @internal + */ + tokenizeViewport(startLineNumber: number, endLineNumber: number): void; + + getLanguageId(): string; + getLanguageIdAtPosition(lineNumber: number, column: number): string; + + setLanguageId(languageId: string): void; + + readonly backgroundTokenizationState: BackgroundTokenizationState; + readonly onBackgroundTokenizationStateChanged: Event<void>; +} + +export const enum BackgroundTokenizationState { + Uninitialized = 0, + InProgress = 1, + Completed = 2, +} diff --git a/src/vs/editor/common/tokens/contiguousTokensStore.ts b/src/vs/editor/common/tokens/contiguousTokensStore.ts index 86f13cfeb5c..b1aac9f8232 100644 --- a/src/vs/editor/common/tokens/contiguousTokensStore.ts +++ b/src/vs/editor/common/tokens/contiguousTokensStore.ts @@ -211,5 +211,7 @@ function getDefaultMetadata(topLevelLanguageId: LanguageId): number { | (FontStyle.None << MetadataConsts.FONT_STYLE_OFFSET) | (ColorId.DefaultForeground << MetadataConsts.FOREGROUND_OFFSET) | (ColorId.DefaultBackground << MetadataConsts.BACKGROUND_OFFSET) + // If there is no grammar, we just take a guess and try to match brackets. + | (MetadataConsts.BALANCED_BRACKETS_MASK) ) >>> 0; } diff --git a/src/vs/editor/common/tokens/sparseTokensStore.ts b/src/vs/editor/common/tokens/sparseTokensStore.ts index 578bf2eecfa..387adb9b518 100644 --- a/src/vs/editor/common/tokens/sparseTokensStore.ts +++ b/src/vs/editor/common/tokens/sparseTokensStore.ts @@ -123,6 +123,11 @@ export class SparseTokensStore { } public addSparseTokens(lineNumber: number, aTokens: LineTokens): LineTokens { + if (aTokens.getLineContent().length === 0) { + // Don't do anything for empty lines + return aTokens; + } + const pieces = this._pieces; if (pieces.length === 0) { diff --git a/src/vs/editor/common/viewLayout/viewLineRenderer.ts b/src/vs/editor/common/viewLayout/viewLineRenderer.ts index 61b0eec236e..1bb33b6cc48 100644 --- a/src/vs/editor/common/viewLayout/viewLineRenderer.ts +++ b/src/vs/editor/common/viewLayout/viewLineRenderer.ts @@ -773,6 +773,18 @@ function _applyRenderWhitespace(input: RenderLineInput, lineContent: string, len isInWhitespace = lineIsEmptyOrWhitespace || charIndex > lastNonWhitespaceIndex; } + if (isInWhitespace && tokenContainsRTL) { + // If the token contains RTL text, breaking it up into multiple line parts + // to render whitespace might affect the browser's bidi layout. + // + // We render whitespace in such tokens only if the whitespace + // is the leading or the trailing whitespace of the line, + // which doesn't affect the browser's bidi layout. + if (charIndex >= firstNonWhitespaceIndex && charIndex <= lastNonWhitespaceIndex) { + isInWhitespace = false; + } + } + if (wasInWhitespace) { // was in whitespace token if (!isInWhitespace || (!useMonospaceOptimizations && tmpIndent >= tabSize)) { @@ -959,7 +971,7 @@ function _renderLine(input: ResolvedRenderLineInput, sb: IStringBuilder): Render sb.appendASCIIString('<span '); if (partContainsRTL) { - sb.appendASCIIString('dir="auto" '); + sb.appendASCIIString('style="unicode-bidi:isolate" '); } sb.appendASCIIString('class="'); sb.appendASCIIString(partRendersWhitespaceWithWidth ? 'mtkz' : partType); diff --git a/src/vs/editor/common/viewModel/modelLineProjection.ts b/src/vs/editor/common/viewModel/modelLineProjection.ts index 8a2380d3b74..a43c41fc870 100644 --- a/src/vs/editor/common/viewModel/modelLineProjection.ts +++ b/src/vs/editor/common/viewModel/modelLineProjection.ts @@ -37,7 +37,9 @@ export interface IModelLineProjection { } export interface ISimpleModel { - getLineTokens(lineNumber: number): LineTokens; + tokenization: { + getLineTokens(lineNumber: number): LineTokens; + }; getLineContent(lineNumber: number): string; getLineLength(lineNumber: number): number; getLineMinColumn(lineNumber: number): number; @@ -211,13 +213,13 @@ class ModelLineProjection implements IModelLineProjection { let lineWithInjections: LineTokens; if (injectionOffsets) { - lineWithInjections = model.getLineTokens(modelLineNumber).withInserted(injectionOffsets.map((offset, idx) => ({ + lineWithInjections = model.tokenization.getLineTokens(modelLineNumber).withInserted(injectionOffsets.map((offset, idx) => ({ offset, text: injectionOptions![idx].content, tokenMetadata: LineTokens.defaultTokenMetadata }))); } else { - lineWithInjections = model.getLineTokens(modelLineNumber); + lineWithInjections = model.tokenization.getLineTokens(modelLineNumber); } for (let outputLineIndex = outputLineIdx; outputLineIndex < outputLineIdx + lineCount; outputLineIndex++) { @@ -339,7 +341,7 @@ class IdentityModelLineProjection implements IModelLineProjection { } public getViewLineData(model: ISimpleModel, modelLineNumber: number, _outputLineIndex: number): ViewLineData { - const lineTokens = model.getLineTokens(modelLineNumber); + const lineTokens = model.tokenization.getLineTokens(modelLineNumber); const lineContent = lineTokens.getLineContent(); return new ViewLineData( lineContent, diff --git a/src/vs/editor/common/viewModel/viewModelDecorations.ts b/src/vs/editor/common/viewModel/viewModelDecorations.ts index 47d1d23ea99..e9db60465a0 100644 --- a/src/vs/editor/common/viewModel/viewModelDecorations.ts +++ b/src/vs/editor/common/viewModel/viewModelDecorations.ts @@ -204,7 +204,7 @@ export function isModelDecorationInString(model: ITextModel, decoration: IModelD */ function testTokensInRange(model: ITextModel, range: Range, callback: (tokenType: StandardTokenType) => boolean): boolean { for (let lineNumber = range.startLineNumber; lineNumber <= range.endLineNumber; lineNumber++) { - const lineTokens = model.getLineTokens(lineNumber); + const lineTokens = model.tokenization.getLineTokens(lineNumber); const isFirstLine = lineNumber === range.startLineNumber; const isEndLine = lineNumber === range.endLineNumber; diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index c6ab57b1186..8595127b0ed 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -53,6 +53,7 @@ export class ViewModel extends Disposable implements IViewModel { private readonly _updateConfigurationViewLineCount: RunOnceScheduler; private _hasFocus: boolean; private _viewportStartLine: number; + private _viewportStartLineIsValid: boolean; private _viewportStartLineTrackedRange: string | null; private _viewportStartLineDelta: number; private readonly _lines: IViewModelLines; @@ -83,6 +84,7 @@ export class ViewModel extends Disposable implements IViewModel { this._updateConfigurationViewLineCount = this._register(new RunOnceScheduler(() => this._updateConfigurationViewLineCountNow(), 0)); this._hasFocus = false; this._viewportStartLine = -1; + this._viewportStartLineIsValid = false; this._viewportStartLineTrackedRange = null; this._viewportStartLineDelta = 0; @@ -120,6 +122,9 @@ export class ViewModel extends Disposable implements IViewModel { if (e.scrollTopChanged) { this._tokenizeViewportSoon.schedule(); } + if (e.scrollTopChanged) { + this._viewportStartLineIsValid = false; + } this._eventDispatcher.emitSingleViewEvent(new viewEvents.ViewScrollChangedEvent(e)); this._eventDispatcher.emitOutgoingEvent(new ScrollChangedEvent( e.oldScrollWidth, e.oldScrollLeft, e.oldScrollHeight, e.oldScrollTop, @@ -193,7 +198,7 @@ export class ViewModel extends Disposable implements IViewModel { const modelVisibleRanges = this._toModelVisibleRanges(viewVisibleRange); for (const modelVisibleRange of modelVisibleRanges) { - this.model.tokenizeViewport(modelVisibleRange.startLineNumber, modelVisibleRange.endLineNumber); + this.model.tokenization.tokenizeViewport(modelVisibleRange.startLineNumber, modelVisibleRange.endLineNumber); } } @@ -380,7 +385,7 @@ export class ViewModel extends Disposable implements IViewModel { this._updateConfigurationViewLineCountNow(); // Recover viewport - if (!this._hasFocus && this.model.getAttachedEditorCount() >= 2 && this._viewportStartLineTrackedRange) { + if (!this._hasFocus && this.model.getAttachedEditorCount() >= 2 && this._viewportStartLineTrackedRange && this._viewportStartLineIsValid) { const modelRange = this.model._getTrackedRange(this._viewportStartLineTrackedRange); if (modelRange) { const viewPosition = this.coordinatesConverter.convertModelPositionToViewPosition(modelRange.getStartPosition()); @@ -624,6 +629,7 @@ export class ViewModel extends Disposable implements IViewModel { */ public setViewport(startLineNumber: number, endLineNumber: number, centeredLineNumber: number): void { this._viewportStartLine = startLineNumber; + this._viewportStartLineIsValid = true; const position = this.coordinatesConverter.convertViewPositionToModelPosition(new Position(startLineNumber, this.getLineMinColumn(startLineNumber))); this._viewportStartLineTrackedRange = this.model._setTrackedRange(this._viewportStartLineTrackedRange, new Range(position.lineNumber, position.column, position.lineNumber, position.column), TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges); const viewportStartLineTop = this.viewLayout.getVerticalOffsetForLineNumber(startLineNumber); @@ -914,7 +920,7 @@ export class ViewModel extends Disposable implements IViewModel { let result = ''; for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) { - const lineTokens = this.model.getLineTokens(lineNumber); + const lineTokens = this.model.tokenization.getLineTokens(lineNumber); const lineContent = lineTokens.getLineContent(); const startOffset = (lineNumber === startLineNumber ? startColumn - 1 : 0); const endOffset = (lineNumber === endLineNumber ? endColumn - 1 : lineContent.length); diff --git a/src/vs/editor/common/viewModel/viewModelLines.ts b/src/vs/editor/common/viewModel/viewModelLines.ts index 2dbfff15b9e..f2cb108aced 100644 --- a/src/vs/editor/common/viewModel/viewModelLines.ts +++ b/src/vs/editor/common/viewModel/viewModelLines.ts @@ -582,10 +582,6 @@ export class ViewModelLinesFromProjectedModel implements IViewModelLines { // visibleColumns stay as they are (this is a bug and needs to be fixed, but it is not a regression) // model-columns must be converted to view-model columns. const result = bracketGuides.map(g => { - /*if (g.onlyForWrappedLines && !viewLineInfo.isWrappedLineContinuation) { - return undefined; - }*/ - if (g.forWrappedLinesAfterColumn !== -1) { const p = this.modelLineProjections[viewLineInfo.modelLineNumber - 1].getViewPositionOfModelPosition(0, g.forWrappedLinesAfterColumn); if (p.lineNumber >= viewLineInfo.modelLineWrappedLineIdx) { @@ -593,9 +589,9 @@ export class ViewModelLinesFromProjectedModel implements IViewModelLines { } } - if (g.forWrappedLinesBeforeColumn !== -1) { - const p = this.modelLineProjections[viewLineInfo.modelLineNumber - 1].getViewPositionOfModelPosition(0, g.forWrappedLinesBeforeColumn); - if (p.lineNumber <= viewLineInfo.modelLineWrappedLineIdx) { + if (g.forWrappedLinesBeforeOrAtColumn !== -1) { + const p = this.modelLineProjections[viewLineInfo.modelLineNumber - 1].getViewPositionOfModelPosition(0, g.forWrappedLinesBeforeOrAtColumn); + if (p.lineNumber < viewLineInfo.modelLineWrappedLineIdx) { return undefined; } } @@ -1200,7 +1196,7 @@ export class ViewModelLinesFromModelAsIs implements IViewModelLines { } public getViewLineData(viewLineNumber: number): ViewLineData { - const lineTokens = this.model.getLineTokens(viewLineNumber); + const lineTokens = this.model.tokenization.getLineTokens(viewLineNumber); const lineContent = lineTokens.getLineContent(); return new ViewLineData( lineContent, diff --git a/src/vs/editor/contrib/bracketMatching/browser/bracketMatching.ts b/src/vs/editor/contrib/bracketMatching/browser/bracketMatching.ts index 401151110d9..37b38180c3a 100644 --- a/src/vs/editor/contrib/bracketMatching/browser/bracketMatching.ts +++ b/src/vs/editor/contrib/bracketMatching/browser/bracketMatching.ts @@ -175,7 +175,7 @@ export class BracketMatchingController extends Disposable implements IEditorCont const brackets = model.bracketPairs.matchBracket(position); let newCursorPosition: Position | null = null; if (brackets) { - if (brackets[0].containsPosition(position)) { + if (brackets[0].containsPosition(position) && !brackets[1].containsPosition(position)) { newCursorPosition = brackets[1].getStartPosition(); } else if (brackets[1].containsPosition(position)) { newCursorPosition = brackets[0].getStartPosition(); @@ -184,7 +184,7 @@ export class BracketMatchingController extends Disposable implements IEditorCont // find the enclosing brackets if the position isn't on a matching bracket const enclosingBrackets = model.bracketPairs.findEnclosingBrackets(position); if (enclosingBrackets) { - newCursorPosition = enclosingBrackets[0].getStartPosition(); + newCursorPosition = enclosingBrackets[1].getStartPosition(); } else { // no enclosing brackets, try the very first next bracket const nextBracket = model.bracketPairs.findNextBracket(position); diff --git a/src/vs/editor/contrib/clipboard/browser/clipboard.ts b/src/vs/editor/contrib/clipboard/browser/clipboard.ts index 0cfb518a71f..e9a71900cf1 100644 --- a/src/vs/editor/contrib/clipboard/browser/clipboard.ts +++ b/src/vs/editor/contrib/clipboard/browser/clipboard.ts @@ -39,9 +39,7 @@ export const CutAction = supportsCut ? registerCommand(new MultiCommand({ kbOpts: ( // Do not bind cut keybindings in the browser, // since browsers do that for us and it avoids security prompts - // the exception to that logic is Safari which needs to go through - // our clipboard service. More info why can be found in the clipboard service. - platform.isNative || platform.isSafari ? { + platform.isNative ? { primary: KeyMod.CtrlCmd | KeyCode.KeyX, win: { primary: KeyMod.CtrlCmd | KeyCode.KeyX, secondary: [KeyMod.Shift | KeyCode.Delete] }, weight: KeybindingWeight.EditorContrib diff --git a/src/vs/editor/contrib/codeAction/browser/codeActionCommands.ts b/src/vs/editor/contrib/codeAction/browser/codeActionCommands.ts index 3921bbb6b91..6e4c7a0b42e 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeActionCommands.ts +++ b/src/vs/editor/contrib/codeAction/browser/codeActionCommands.ts @@ -154,9 +154,11 @@ export async function applyCodeAction( codeActionIsPreferred: boolean; }; type ApplyCodeEventClassification = { - codeActionTitle: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'mjbvz'; comment: 'The display label of the applied code action' }; - codeActionKind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'mjbvz'; comment: 'The kind (refactor, quickfix) of the applied code action' }; - codeActionIsPreferred: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'mjbvz'; comment: 'Was the code action marked as being a preferred action?' }; + codeActionTitle: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The display label of the applied code action' }; + codeActionKind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The kind (refactor, quickfix) of the applied code action' }; + codeActionIsPreferred: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Was the code action marked as being a preferred action?' }; + owner: 'mjbvz'; + comment: 'Event used to gain insights into which code actions are being triggered'; }; telemetryService.publicLog2<ApplyCodeActionEvent, ApplyCodeEventClassification>('codeAction.applyCodeAction', { diff --git a/src/vs/editor/contrib/comment/browser/blockCommentCommand.ts b/src/vs/editor/contrib/comment/browser/blockCommentCommand.ts index b0a162d3422..08c999bf42b 100644 --- a/src/vs/editor/contrib/comment/browser/blockCommentCommand.ts +++ b/src/vs/editor/contrib/comment/browser/blockCommentCommand.ts @@ -170,7 +170,7 @@ export class BlockCommentCommand implements ICommand { const startLineNumber = this._selection.startLineNumber; const startColumn = this._selection.startColumn; - model.tokenizeIfCheap(startLineNumber); + model.tokenization.tokenizeIfCheap(startLineNumber); const languageId = model.getLanguageIdAtPosition(startLineNumber, startColumn); const config = this.languageConfigurationService.getLanguageConfiguration(languageId).comments; if (!config || !config.blockCommentStartToken || !config.blockCommentEndToken) { diff --git a/src/vs/editor/contrib/comment/browser/lineCommentCommand.ts b/src/vs/editor/contrib/comment/browser/lineCommentCommand.ts index c1964f40201..97770268095 100644 --- a/src/vs/editor/contrib/comment/browser/lineCommentCommand.ts +++ b/src/vs/editor/contrib/comment/browser/lineCommentCommand.ts @@ -85,7 +85,7 @@ export class LineCommentCommand implements ICommand { */ private static _gatherPreflightCommentStrings(model: ITextModel, startLineNumber: number, endLineNumber: number, languageConfigurationService: ILanguageConfigurationService): ILinePreflightData[] | null { - model.tokenizeIfCheap(startLineNumber); + model.tokenization.tokenizeIfCheap(startLineNumber); const languageId = model.getLanguageIdAtPosition(startLineNumber, 1); const config = languageConfigurationService.getLanguageConfiguration(languageId).comments; @@ -282,7 +282,7 @@ export class LineCommentCommand implements ICommand { * Given an unsuccessful analysis, delegate to the block comment command */ private _executeBlockComment(model: ITextModel, builder: IEditOperationBuilder, s: Selection): void { - model.tokenizeIfCheap(s.startLineNumber); + model.tokenization.tokenizeIfCheap(s.startLineNumber); let languageId = model.getLanguageIdAtPosition(s.startLineNumber, 1); const config = this.languageConfigurationService.getLanguageConfiguration(languageId).comments; if (!config || !config.blockCommentStartToken || !config.blockCommentEndToken) { diff --git a/src/vs/editor/contrib/comment/test/browser/lineCommentCommand.test.ts b/src/vs/editor/contrib/comment/test/browser/lineCommentCommand.test.ts index 4bce445798f..a451d61ecac 100644 --- a/src/vs/editor/contrib/comment/test/browser/lineCommentCommand.test.ts +++ b/src/vs/editor/contrib/comment/test/browser/lineCommentCommand.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { Selection } from 'vs/editor/common/core/selection'; import { ICommand } from 'vs/editor/common/editorCommon'; import { EncodedTokenizationResult, ColorId, IState, MetadataConsts, TokenizationRegistry } from 'vs/editor/common/languages'; @@ -15,7 +15,6 @@ import { ILanguageService } from 'vs/editor/common/languages/language'; import { ILinePreflightData, IPreflightData, ISimpleModel, LineCommentCommand, Type } from 'vs/editor/contrib/comment/browser/lineCommentCommand'; import { testCommand } from 'vs/editor/test/browser/testCommand'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { MockMode } from 'vs/editor/test/common/mocks/mockMode'; import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; function createTestCommandHelper(commentsConfig: CommentRule, commandFactory: (accessor: ServicesAccessor, selection: Selection) => ICommand): (lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection) => void { @@ -1083,13 +1082,15 @@ suite('Editor Contrib - Line Comment in mixed modes', () => { const OUTER_LANGUAGE_ID = 'outerMode'; const INNER_LANGUAGE_ID = 'innerMode'; - class OuterMode extends MockMode { + class OuterMode extends Disposable { + private readonly languageId = OUTER_LANGUAGE_ID; constructor( commentsConfig: CommentRule, @ILanguageService languageService: ILanguageService, @ILanguageConfigurationService languageConfigurationService: ILanguageConfigurationService ) { - super(OUTER_LANGUAGE_ID); + super(); + this._register(languageService.registerLanguage({ id: this.languageId })); this._register(languageConfigurationService.register(this.languageId, { comments: commentsConfig })); @@ -1115,12 +1116,15 @@ suite('Editor Contrib - Line Comment in mixed modes', () => { } } - class InnerMode extends MockMode { + class InnerMode extends Disposable { + private readonly languageId = INNER_LANGUAGE_ID; constructor( commentsConfig: CommentRule, + @ILanguageService languageService: ILanguageService, @ILanguageConfigurationService languageConfigurationService: ILanguageConfigurationService ) { - super(INNER_LANGUAGE_ID); + super(); + this._register(languageService.registerLanguage({ id: this.languageId })); this._register(languageConfigurationService.register(this.languageId, { comments: commentsConfig })); diff --git a/src/vs/editor/contrib/contextmenu/browser/contextmenu.ts b/src/vs/editor/contrib/contextmenu/browser/contextmenu.ts index 6c8897127bc..1c7419f1053 100644 --- a/src/vs/editor/contrib/contextmenu/browser/contextmenu.ts +++ b/src/vs/editor/contrib/contextmenu/browser/contextmenu.ts @@ -62,6 +62,9 @@ export class ContextMenuController implements IEditorContribution { } })); this._toDispose.add(this._editor.onKeyDown((e: IKeyboardEvent) => { + if (!this._editor.getOption(EditorOption.contextmenu)) { + return; // Context menu is turned off through configuration + } if (e.keyCode === KeyCode.ContextMenu) { // Chrome is funny like that e.preventDefault(); diff --git a/src/vs/editor/contrib/gotoError/browser/gotoErrorWidget.ts b/src/vs/editor/contrib/gotoError/browser/gotoErrorWidget.ts index 47a28e5d55c..646b0758449 100644 --- a/src/vs/editor/contrib/gotoError/browser/gotoErrorWidget.ts +++ b/src/vs/editor/contrib/gotoError/browser/gotoErrorWidget.ts @@ -9,7 +9,6 @@ import { IAction } from 'vs/base/common/actions'; import { isNonEmptyArray } from 'vs/base/common/arrays'; import { Color } from 'vs/base/common/color'; import { Emitter, Event } from 'vs/base/common/event'; -import { getBaseLabel } from 'vs/base/common/labels'; import { DisposableStore, dispose } from 'vs/base/common/lifecycle'; import { basename } from 'vs/base/common/resources'; import { ScrollbarVisibility } from 'vs/base/common/scrollable'; @@ -169,7 +168,7 @@ class MessageWidget { let relatedResource = document.createElement('a'); relatedResource.classList.add('filename'); - relatedResource.innerText = `${getBaseLabel(related.resource)}(${related.startLineNumber}, ${related.startColumn}): `; + relatedResource.innerText = `${this._labelService.getUriBasenameLabel(related.resource)}(${related.startLineNumber}, ${related.startColumn}): `; relatedResource.title = this._labelService.getUriLabel(related.resource); this._relatedDiagnostics.set(relatedResource, related); diff --git a/src/vs/editor/contrib/gotoSymbol/browser/link/goToDefinitionAtPosition.ts b/src/vs/editor/contrib/gotoSymbol/browser/link/goToDefinitionAtPosition.ts index e6a7f84ee0d..61ef2f26dd1 100644 --- a/src/vs/editor/contrib/gotoSymbol/browser/link/goToDefinitionAtPosition.ts +++ b/src/vs/editor/contrib/gotoSymbol/browser/link/goToDefinitionAtPosition.ts @@ -19,7 +19,6 @@ import { Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model'; -import { IFoundBracket } from 'vs/editor/common/textModelBracketPairs'; import { LocationLink } from 'vs/editor/common/languages'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; @@ -219,7 +218,7 @@ export class GotoDefinitionAtPositionEditorContribution implements IEditorContri } private getPreviewValue(textEditorModel: ITextModel, startLineNumber: number, result: LocationLink) { - let rangeToUse = result.targetSelectionRange ? result.range : this.getPreviewRangeBasedOnBrackets(textEditorModel, startLineNumber); + let rangeToUse = result.range; const numberOfLinesInRange = rangeToUse.endLineNumber - rangeToUse.startLineNumber; if (numberOfLinesInRange >= GotoDefinitionAtPositionEditorContribution.MAX_SOURCE_PREVIEW_LINES) { rangeToUse = this.getPreviewRangeBasedOnIndentation(textEditorModel, startLineNumber); @@ -258,52 +257,6 @@ export class GotoDefinitionAtPositionEditorContribution implements IEditorContri return new Range(startLineNumber, 1, endLineNumber + 1, 1); } - private getPreviewRangeBasedOnBrackets(textEditorModel: ITextModel, startLineNumber: number) { - const maxLineNumber = Math.min(textEditorModel.getLineCount(), startLineNumber + GotoDefinitionAtPositionEditorContribution.MAX_SOURCE_PREVIEW_LINES); - - const brackets: IFoundBracket[] = []; - - let ignoreFirstEmpty = true; - let currentBracket = textEditorModel.bracketPairs.findNextBracket(new Position(startLineNumber, 1)); - while (currentBracket !== null) { - - if (brackets.length === 0) { - brackets.push(currentBracket); - } else { - const lastBracket = brackets[brackets.length - 1]; - if (lastBracket.open[0] === currentBracket.open[0] && lastBracket.isOpen && !currentBracket.isOpen) { - brackets.pop(); - } else { - brackets.push(currentBracket); - } - - if (brackets.length === 0) { - if (ignoreFirstEmpty) { - ignoreFirstEmpty = false; - } else { - return new Range(startLineNumber, 1, currentBracket.range.endLineNumber + 1, 1); - } - } - } - - const maxColumn = textEditorModel.getLineMaxColumn(startLineNumber); - let nextLineNumber = currentBracket.range.endLineNumber; - let nextColumn = currentBracket.range.endColumn; - if (maxColumn === currentBracket.range.endColumn) { - nextLineNumber++; - nextColumn = 1; - } - - if (nextLineNumber > maxLineNumber) { - return new Range(startLineNumber, 1, maxLineNumber + 1, 1); - } - - currentBracket = textEditorModel.bracketPairs.findNextBracket(new Position(nextLineNumber, nextColumn)); - } - - return new Range(startLineNumber, 1, maxLineNumber + 1, 1); - } - private addDecoration(range: Range, hoverMessage: MarkdownString): void { const newDecorations: IModelDeltaDecoration = { diff --git a/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesTree.ts b/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesTree.ts index 787120fc1e7..a39f602999f 100644 --- a/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesTree.ts +++ b/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesTree.ts @@ -12,7 +12,6 @@ import { IIdentityProvider, IKeyboardNavigationLabelProvider, IListVirtualDelega import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { IAsyncDataSource, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; import { createMatches, FuzzyScore, IMatch } from 'vs/base/common/filters'; -import { getBaseLabel } from 'vs/base/common/labels'; import { Disposable } from 'vs/base/common/lifecycle'; import { basename, dirname } from 'vs/base/common/resources'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; @@ -113,7 +112,7 @@ class FileReferencesTemplate extends Disposable { constructor( container: HTMLElement, - @ILabelService private readonly _uriLabel: ILabelService, + @ILabelService private readonly _labelService: ILabelService, @IThemeService themeService: IThemeService, ) { super(); @@ -129,7 +128,11 @@ class FileReferencesTemplate extends Disposable { set(element: FileReferences, matches: IMatch[]) { let parent = dirname(element.uri); - this.file.setLabel(getBaseLabel(element.uri), this._uriLabel.getUriLabel(parent, { relative: true }), { title: this._uriLabel.getUriLabel(element.uri), matches }); + this.file.setLabel( + this._labelService.getUriBasenameLabel(element.uri), + this._labelService.getUriLabel(parent, { relative: true }), + { title: this._labelService.getUriLabel(element.uri), matches } + ); const len = element.children.length; this.badge.setCount(len); if (len > 1) { diff --git a/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesWidget.ts b/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesWidget.ts index baee24e0c7b..917b7f208ab 100644 --- a/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesWidget.ts +++ b/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesWidget.ts @@ -259,7 +259,6 @@ export class ReferenceWidget extends peekView.PeekViewWidget { } override show(where: IRange) { - this.editor.revealRangeInCenterIfOutsideViewport(where, ScrollType.Smooth); super.show(where, this.layoutData.heightInLines || 18); } diff --git a/src/vs/editor/contrib/indentation/browser/indentation.ts b/src/vs/editor/contrib/indentation/browser/indentation.ts index c7ea4ee4b3a..2f7bcfd1eae 100644 --- a/src/vs/editor/contrib/indentation/browser/indentation.ts +++ b/src/vs/editor/contrib/indentation/browser/indentation.ts @@ -473,7 +473,7 @@ export class AutoIndentOnPaste implements IEditorContribution { return; } - if (!model.isCheapToTokenize(range.getStartPosition().lineNumber)) { + if (!model.tokenization.isCheapToTokenize(range.getStartPosition().lineNumber)) { return; } const autoIndent = this.editor.getOption(EditorOption.autoIndent); @@ -546,14 +546,16 @@ export class AutoIndentOnPaste implements IEditorContribution { if (startLineNumber !== range.endLineNumber) { let virtualModel = { - getLineTokens: (lineNumber: number) => { - return model.getLineTokens(lineNumber); - }, - getLanguageId: () => { - return model.getLanguageId(); - }, - getLanguageIdAtPosition: (lineNumber: number, column: number) => { - return model.getLanguageIdAtPosition(lineNumber, column); + tokenization: { + getLineTokens: (lineNumber: number) => { + return model.tokenization.getLineTokens(lineNumber); + }, + getLanguageId: () => { + return model.getLanguageId(); + }, + getLanguageIdAtPosition: (lineNumber: number, column: number) => { + return model.getLanguageIdAtPosition(lineNumber, column); + }, }, getLineContent: (lineNumber: number) => { if (lineNumber === firstLineNumber) { @@ -597,12 +599,12 @@ export class AutoIndentOnPaste implements IEditorContribution { } private shouldIgnoreLine(model: ITextModel, lineNumber: number): boolean { - model.forceTokenization(lineNumber); + model.tokenization.forceTokenization(lineNumber); let nonWhitespaceColumn = model.getLineFirstNonWhitespaceColumn(lineNumber); if (nonWhitespaceColumn === 0) { return true; } - let tokens = model.getLineTokens(lineNumber); + let tokens = model.tokenization.getLineTokens(lineNumber); if (tokens.getCount() > 0) { let firstNonWhitespaceTokenIndex = tokens.findTokenIndexAtOffset(nonWhitespaceColumn); if (firstNonWhitespaceTokenIndex >= 0 && tokens.getStandardTokenType(firstNonWhitespaceTokenIndex) === StandardTokenType.Comment) { diff --git a/src/vs/editor/contrib/inlayHints/browser/inlayHints.ts b/src/vs/editor/contrib/inlayHints/browser/inlayHints.ts index a1f933965cc..01ab877a610 100644 --- a/src/vs/editor/contrib/inlayHints/browser/inlayHints.ts +++ b/src/vs/editor/contrib/inlayHints/browser/inlayHints.ts @@ -139,8 +139,8 @@ export class InlayHintsFragments { return new Range(line, word.startColumn, line, word.endColumn); } - model.tokenizeIfCheap(line); - const tokens = model.getLineTokens(line); + model.tokenization.tokenizeIfCheap(line); + const tokens = model.tokenization.getLineTokens(line); const offset = position.column - 1; const idx = tokens.findTokenIndexAtOffset(offset); diff --git a/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts b/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts index a1974f92176..38515c135bd 100644 --- a/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts +++ b/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { ModifierKeyEmitter } from 'vs/base/browser/dom'; import { isNonEmptyArray } from 'vs/base/common/arrays'; import { RunOnceScheduler } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; @@ -19,7 +20,7 @@ import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Range } from 'vs/editor/common/core/range'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import * as languages from 'vs/editor/common/languages'; -import { IModelDeltaDecoration, InjectedTextCursorStops, ITextModel, TrackedRangeStickiness } from 'vs/editor/common/model'; +import { IModelDeltaDecoration, InjectedTextCursorStops, InjectedTextOptions, ITextModel, TrackedRangeStickiness } from 'vs/editor/common/model'; import { ModelDecorationInjectedTextOptions } from 'vs/editor/common/model/textModel'; import { IFeatureDebounceInformation, ILanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; @@ -80,6 +81,17 @@ class ActiveInlayHintInfo { constructor(readonly part: RenderedInlayHintLabelPart, readonly hasTriggerModifier: boolean) { } } +type InlayHintDecorationRenderInfo = { + item: InlayHintItem; + decoration: IModelDeltaDecoration; + classNameRef: ClassNameReference; +}; + +const enum RenderMode { + Normal, + Invisible +} + // --- controller export class InlayHintsController implements IEditorContribution { @@ -95,9 +107,10 @@ export class InlayHintsController implements IEditorContribution { private readonly _disposables = new DisposableStore(); private readonly _sessionDisposables = new DisposableStore(); private readonly _debounceInfo: IFeatureDebounceInformation; - private readonly _decorationsMetadata = new Map<string, { item: InlayHintItem; classNameRef: IDisposable }>(); + private readonly _decorationsMetadata = new Map<string, InlayHintDecorationRenderInfo>(); private readonly _ruleFactory = new DynamicCssRules(this._editor); + private _activeRenderMode = RenderMode.Normal; private _activeInlayHintPart?: ActiveInlayHintInfo; constructor( @@ -119,6 +132,7 @@ export class InlayHintsController implements IEditorContribution { } })); this._update(); + } dispose(): void { @@ -131,7 +145,8 @@ export class InlayHintsController implements IEditorContribution { this._sessionDisposables.clear(); this._removeAllDecorations(); - if (!this._editor.getOption(EditorOption.inlayHints).enabled) { + const options = this._editor.getOption(EditorOption.inlayHints); + if (options.enabled === 'off') { return; } @@ -215,9 +230,40 @@ export class InlayHintsController implements IEditorContribution { scheduler.schedule(delay); })); + if (options.enabled === 'on') { + // different "on" modes: always + this._activeRenderMode = RenderMode.Normal; + } else { + // different "on" modes: offUnlessPressed, or onUnlessPressed + let defaultMode: RenderMode; + let altMode: RenderMode; + if (options.enabled === 'onUnlessPressed') { + defaultMode = RenderMode.Normal; + altMode = RenderMode.Invisible; + } else { + defaultMode = RenderMode.Invisible; + altMode = RenderMode.Normal; + } + this._activeRenderMode = defaultMode; + + this._sessionDisposables.add(ModifierKeyEmitter.getInstance().event(e => { + if (!this._editor.hasModel()) { + return; + } + const newRenderMode = e.altKey && e.ctrlKey ? altMode : defaultMode; + if (newRenderMode !== this._activeRenderMode) { + this._activeRenderMode = newRenderMode; + const ranges = this._getHintsRanges(); + const copies = this._copyInlayHintsWithCurrentAnchor(this._editor.getModel()); + this._updateHintsDecorators(ranges, copies); + scheduler.schedule(0); + } + })); + } + // mouse gestures + this._sessionDisposables.add(this._installDblClickGesture(() => scheduler.schedule(0))); this._sessionDisposables.add(this._installLinkGesture()); - this._sessionDisposables.add(this._installDblClickGesture()); this._sessionDisposables.add(this._installContextMenu()); } @@ -253,16 +299,11 @@ export class InlayHintsController implements IEditorContribution { const lineNumber = labelPart.item.hint.position.lineNumber; const range = new Range(lineNumber, 1, lineNumber, model.getLineMaxColumn(lineNumber)); - const lineHints = new Set<InlayHintItem>(); - for (const data of this._decorationsMetadata.values()) { - if (range.containsRange(data.item.anchor.range)) { - lineHints.add(data.item); - } - } - this._updateHintsDecorators([range], Array.from(lineHints)); + const lineHints = this._getInlineHintsForRange(range); + this._updateHintsDecorators([range], lineHints); sessionStore.add(toDisposable(() => { this._activeInlayHintPart = undefined; - this._updateHintsDecorators([range], Array.from(lineHints)); + this._updateHintsDecorators([range], lineHints); })); })); store.add(gesture.onCancel(() => sessionStore.clear())); @@ -282,7 +323,17 @@ export class InlayHintsController implements IEditorContribution { return store; } - private _installDblClickGesture(): IDisposable { + private _getInlineHintsForRange(range: Range) { + const lineHints = new Set<InlayHintItem>(); + for (const data of this._decorationsMetadata.values()) { + if (range.containsRange(data.item.anchor.range)) { + lineHints.add(data.item); + } + } + return Array.from(lineHints); + } + + private _installDblClickGesture(updateInlayHints: Function): IDisposable { return this._editor.onMouseUp(async e => { if (e.event.detail !== 2) { return; @@ -296,6 +347,7 @@ export class InlayHintsController implements IEditorContribution { if (isNonEmptyArray(part.item.hint.textEdits)) { const edits = part.item.hint.textEdits.map(edit => EditOperation.replace(Range.lift(edit.range), edit.text)); this._editor.executeEdits('inlayHint.default', edits); + updateInlayHints(); } }); } @@ -336,6 +388,13 @@ export class InlayHintsController implements IEditorContribution { } private _cacheHintsForFastRestore(model: ITextModel): void { + const hints = this._copyInlayHintsWithCurrentAnchor(model); + this._inlayHintsCache.set(model, hints); + } + + // return inlay hints but with an anchor that reflects "updates" + // that happens after receiving them, e.g adding new lines before a hint + private _copyInlayHintsWithCurrentAnchor(model: ITextModel): InlayHintItem[] { const items = new Map<InlayHintItem, InlayHintItem>(); for (const [id, obj] of this._decorationsMetadata) { if (items.has(obj.item)) { @@ -343,16 +402,15 @@ export class InlayHintsController implements IEditorContribution { // but they will all uses the same range continue; } - let value = obj.item; const range = model.getDecorationRange(id); if (range) { // update range with whatever the editor has tweaked it to const anchor = new InlayHintAnchor(range, obj.item.anchor.direction); - value = obj.item.with({ anchor }); + const copy = obj.item.with({ anchor }); + items.set(obj.item, copy); } - items.set(obj.item, value); } - this._inlayHintsCache.set(model, Array.from(items.values())); + return Array.from(items.values()); } private _getHintsRanges(): Range[] { @@ -374,8 +432,15 @@ export class InlayHintsController implements IEditorContribution { private _updateHintsDecorators(ranges: readonly Range[], items: readonly InlayHintItem[]): void { // utils to collect/create injected text decorations - const newDecorationsData: { item: InlayHintItem; decoration: IModelDeltaDecoration; classNameRef: IDisposable }[] = []; + const newDecorationsData: InlayHintDecorationRenderInfo[] = []; const addInjectedText = (item: InlayHintItem, ref: ClassNameReference, content: string, cursorStops: InjectedTextCursorStops, attachedData?: RenderedInlayHintLabelPart): void => { + const opts: InjectedTextOptions = { + content, + inlineClassNameAffectsLetterSpacing: true, + inlineClassName: ref.className, + cursorStops, + attachedData + }; newDecorationsData.push({ item, classNameRef: ref, @@ -387,13 +452,7 @@ export class InlayHintsController implements IEditorContribution { showIfCollapsed: item.anchor.range.isEmpty(), // "original" range is empty collapseOnReplaceEdit: !item.anchor.range.isEmpty(), stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges, - [item.anchor.direction]: { - content, - inlineClassNameAffectsLetterSpacing: true, - inlineClassName: ref.className, - cursorStops, - attachedData - } + [item.anchor.direction]: this._activeRenderMode === RenderMode.Normal ? opts : undefined } } }); @@ -507,7 +566,7 @@ export class InlayHintsController implements IEditorContribution { const newDecorationIds = this._editor.deltaDecorations(decorationIdsToReplace, newDecorationsData.map(d => d.decoration)); for (let i = 0; i < newDecorationIds.length; i++) { const data = newDecorationsData[i]; - this._decorationsMetadata.set(newDecorationIds[i], { item: data.item, classNameRef: data.classNameRef }); + this._decorationsMetadata.set(newDecorationIds[i], data); } } @@ -571,7 +630,6 @@ function fixSpace(str: string): string { } - CommandsRegistry.registerCommand('_executeInlayHintProvider', async (accessor, ...args: [URI, IRange]): Promise<languages.InlayHint[]> => { const [uri, range] = args; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/ghostTextHoverParticipant.ts b/src/vs/editor/contrib/inlineCompletions/browser/ghostTextHoverParticipant.ts index 7ab26922483..6a2fe0eb945 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/ghostTextHoverParticipant.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/ghostTextHoverParticipant.ts @@ -20,6 +20,7 @@ import { ICommandService } from 'vs/platform/commands/common/commands'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { inlineSuggestCommitId } from 'vs/editor/contrib/inlineCompletions/browser/consts'; +import { Command } from 'vs/editor/common/languages'; export class InlineCompletionsHover implements IHoverPart { constructor( @@ -39,6 +40,10 @@ export class InlineCompletionsHover implements IHoverPart { public hasMultipleSuggestions(): Promise<boolean> { return this.controller.hasMultipleInlineCompletions(); } + + public get commands(): Command[] { + return this.controller.activeModel?.activeInlineCompletionsModel?.completionSession.value?.commands || []; + } } export class InlineCompletionsHoverParticipant implements IEditorHoverParticipant<InlineCompletionsHover> { @@ -100,6 +105,7 @@ export class InlineCompletionsHoverParticipant implements IEditorHoverParticipan this.renderScreenReaderText(context, part, disposableStore); } + // TODO@hediet: deprecate MenuId.InlineCompletionsActions const menu = disposableStore.add(this._menuService.createMenu( MenuId.InlineCompletionsActions, this._contextKeyService @@ -131,6 +137,14 @@ export class InlineCompletionsHoverParticipant implements IEditorHoverParticipan } }); + for (const command of part.commands) { + context.statusBar.addAction({ + label: command.title, + commandId: command.id, + run: () => this._commandService.executeCommand(command.id, ...(command.arguments || [])) + }); + } + for (const [_, group] of menu.getActions()) { for (const action of group) { if (action instanceof MenuItemAction) { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/ghostTextWidget.ts b/src/vs/editor/contrib/inlineCompletions/browser/ghostTextWidget.ts index ee134296862..e928c512e02 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/ghostTextWidget.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/ghostTextWidget.ts @@ -81,7 +81,7 @@ export class GhostTextWidget extends Disposable { if (!this.editor.hasModel() || !ghostText || this.disposed) { this.partsWidget.clear(); this.additionalLinesWidget.clear(); - this.replacementDecoration.setDecorations([]); + this.replacementDecoration.clear(); return; } @@ -211,10 +211,18 @@ class DisposableDecorations { } public setDecorations(decorations: IModelDeltaDecoration[]): void { - this.decorationIds = this.editor.deltaDecorations(this.decorationIds, decorations); + // Using change decorations ensures that we update the id's before some event handler is called. + this.editor.changeDecorations(accessor => { + this.decorationIds = accessor.deltaDecorations(this.decorationIds, decorations); + }); + } + + public clear(): void { + this.setDecorations([]); } + public dispose(): void { - this.editor.deltaDecorations(this.decorationIds, []); + this.clear(); } } @@ -231,7 +239,6 @@ interface InsertedInlineText { class DecorationsWidget implements IDisposable { private decorationIds: string[] = []; - private disposableStore: DisposableStore = new DisposableStore(); constructor( private readonly editor: ICodeEditor @@ -240,17 +247,16 @@ class DecorationsWidget implements IDisposable { public dispose(): void { this.clear(); - this.disposableStore.dispose(); } public clear(): void { - this.editor.deltaDecorations(this.decorationIds, []); - this.disposableStore.clear(); + // Using change decorations ensures that we update the id's before some event handler is called. + this.editor.changeDecorations(accessor => { + this.decorationIds = accessor.deltaDecorations(this.decorationIds, []); + }); } public setParts(lineNumber: number, parts: InsertedInlineText[], hiddenText?: HiddenText): void { - this.disposableStore.clear(); - const textModel = this.editor.getModel(); if (!textModel) { return; @@ -267,16 +273,19 @@ class DecorationsWidget implements IDisposable { }); } - this.decorationIds = this.editor.deltaDecorations(this.decorationIds, parts.map<IModelDeltaDecoration>(p => { - return ({ - range: Range.fromPositions(new Position(lineNumber, p.column)), - options: { - description: 'ghost-text', - after: { content: p.text, inlineClassName: p.preview ? 'ghost-text-decoration-preview' : 'ghost-text-decoration', cursorStops: InjectedTextCursorStops.Left }, - showIfCollapsed: true, - } - }); - }).concat(hiddenTextDecorations)); + // Using change decorations ensures that we update the id's before some event handler is called. + this.editor.changeDecorations(accessor => { + this.decorationIds = accessor.deltaDecorations(this.decorationIds, parts.map<IModelDeltaDecoration>(p => { + return ({ + range: Range.fromPositions(new Position(lineNumber, p.column)), + options: { + description: 'ghost-text', + after: { content: p.text, inlineClassName: p.preview ? 'ghost-text-decoration-preview' : 'ghost-text-decoration', cursorStops: InjectedTextCursorStops.Left }, + showIfCollapsed: true, + } + }); + }).concat(hiddenTextDecorations)); + }); } } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts index 002b6262fcb..e7870b9c77d 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts @@ -15,7 +15,7 @@ import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { ITextModel } from 'vs/editor/common/model'; -import { InlineCompletion, InlineCompletionContext, InlineCompletions, InlineCompletionsProvider, InlineCompletionTriggerKind } from 'vs/editor/common/languages'; +import { Command, InlineCompletion, InlineCompletionContext, InlineCompletions, InlineCompletionsProvider, InlineCompletionTriggerKind } from 'vs/editor/common/languages'; import { BaseGhostTextWidgetModel, GhostText, GhostTextReplacement, GhostTextWidgetModel } from 'vs/editor/contrib/inlineCompletions/browser/ghostText'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { inlineSuggestCommitId } from 'vs/editor/contrib/inlineCompletions/browser/consts'; @@ -32,6 +32,7 @@ import { assertNever } from 'vs/base/common/types'; import { matchesSubString } from 'vs/base/common/filters'; import { getReadonlyEmptyArray } from 'vs/editor/contrib/inlineCompletions/browser/utils'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { CursorChangeReason } from 'vs/editor/common/cursorEvents'; export class InlineCompletionsModel extends Disposable implements GhostTextWidgetModel { protected readonly onDidChangeEmitter = new Emitter<void>(); @@ -84,7 +85,8 @@ export class InlineCompletionsModel extends Disposable implements GhostTextWidge this._register( this.editor.onDidChangeCursorPosition((e) => { - if (this.session && !this.session.isValid) { + if (e.reason === CursorChangeReason.Explicit || + this.session && !this.session.isValid) { this.hide(); } }) @@ -252,6 +254,9 @@ export class InlineCompletionsSession extends BaseGhostTextWidgetModel { })); this._register(this.editor.onDidChangeCursorPosition((e) => { + if (e.reason === CursorChangeReason.Explicit) { + return; + } // Ghost text depends on the cursor position this.cache.value?.updateRanges(); if (this.cache.value) { @@ -398,10 +403,14 @@ export class InlineCompletionsSession extends BaseGhostTextWidgetModel { if (!currentCompletion) { return undefined; } + const cursorPosition = this.editor.getPosition(); + if (currentCompletion.range.getEndPosition().isBefore(cursorPosition)) { + return undefined; + } const mode = this.editor.getOptions().get(EditorOption.inlineSuggest).mode; - const ghostText = inlineCompletionToGhostText(currentCompletion, this.editor.getModel(), mode, this.editor.getPosition()); + const ghostText = inlineCompletionToGhostText(currentCompletion, this.editor.getModel(), mode, cursorPosition); if (ghostText) { if (ghostText.isEmpty()) { return undefined; @@ -538,6 +547,11 @@ export class InlineCompletionsSession extends BaseGhostTextWidgetModel { this.onDidChangeEmitter.fire(); } + + public get commands(): Command[] { + const lists = new Set(this.cache.value?.completions.map(c => c.inlineCompletion.sourceInlineCompletions) || []); + return [...lists].flatMap(l => l.commands || []); + } } export class UpdateOperation implements IDisposable { @@ -555,6 +569,7 @@ export class UpdateOperation implements IDisposable { */ export class SynchronizedInlineCompletionsCache extends Disposable { public readonly completions: readonly CachedInlineCompletion[]; + private isDisposing = false; constructor( completionsSource: TrackedInlineCompletions, @@ -574,6 +589,7 @@ export class SynchronizedInlineCompletionsCache extends Disposable { })) ); this._register(toDisposable(() => { + this.isDisposing = true; editor.deltaDecorations(decorationIds, []); })); @@ -586,7 +602,11 @@ export class SynchronizedInlineCompletionsCache extends Disposable { this._register(completionsSource); } - public updateRanges() { + public updateRanges(): void { + if (this.isDisposing) { + return; + } + let hasChanged = false; const model = this.editor.getModel(); for (const c of this.completions) { @@ -784,7 +804,7 @@ function closeBrackets(text: string, position: Position, model: ITextModel, lang const lineStart = model.getLineContent(position.lineNumber).substring(0, position.column - 1); const newLine = lineStart + text; - const newTokens = model.tokenizeLineWithEdit(position, newLine.length - (position.column - 1), text); + const newTokens = model.tokenization.tokenizeLineWithEdit(position, newLine.length - (position.column - 1), text); const slicedTokens = newTokens?.sliceAndInflate(position.column - 1, newLine.length, 0); if (!slicedTokens) { return text; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/suggestWidgetPreviewModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/suggestWidgetPreviewModel.ts index 5a97c173751..0391d402bc1 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/suggestWidgetPreviewModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/suggestWidgetPreviewModel.ts @@ -8,7 +8,7 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; -import { InlineCompletionTriggerKind, SelectedSuggestionInfo } from 'vs/editor/common/languages'; +import { CompletionItemKind, InlineCompletionTriggerKind, SelectedSuggestionInfo } from 'vs/editor/common/languages'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { SharedInlineCompletionCache } from 'vs/editor/contrib/inlineCompletions/browser/ghostTextModel'; import { BaseGhostTextWidgetModel, GhostText } from './ghostText'; @@ -99,6 +99,17 @@ export class SuggestWidgetPreviewModel extends BaseGhostTextWidgetModel { const position = this.editor.getPosition(); + if ( + state.selectedItem.isSnippetText || + state.selectedItem.completionItemKind === CompletionItemKind.Snippet || + state.selectedItem.completionItemKind === CompletionItemKind.File || + state.selectedItem.completionItemKind === CompletionItemKind.Folder + ) { + // Don't ask providers for these types of suggestions. + this.cache.clear(); + return; + } + const promise = createCancelablePromise(async token => { let result: TrackedInlineCompletions; try { diff --git a/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsProvider.test.ts b/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsProvider.test.ts index 4277c79d526..d144f3e7e0f 100644 --- a/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsProvider.test.ts +++ b/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsProvider.test.ts @@ -526,21 +526,19 @@ suite('Inline Completions', () => { context.keyboardType('hello'); await timeout(100); - context.cursorLeft(); // Cause the ghost text to update - context.cursorRight(); + // Update ghost text + context.keyboardType('w'); + context.leftDelete(); await timeout(2000); assert.deepStrictEqual(provider.getAndClearCallHistory(), [ - { - position: '(2,6)', - text: 'hello\nhello', - triggerKind: 0, - } + { position: '(2,6)', triggerKind: 0, text: 'hello\nhello' }, ]); assert.deepStrictEqual(context.getAndClearViewStates(), [ '', + 'hello\n', 'hello[world]\n', 'hello\n', 'hello\nhello[world]', diff --git a/src/vs/editor/contrib/linesOperations/browser/moveLinesCommand.ts b/src/vs/editor/contrib/linesOperations/browser/moveLinesCommand.ts index 81d40b01d43..a8d87224496 100644 --- a/src/vs/editor/contrib/linesOperations/browser/moveLinesCommand.ts +++ b/src/vs/editor/contrib/linesOperations/browser/moveLinesCommand.ts @@ -14,7 +14,7 @@ import { CompleteEnterAction, IndentAction } from 'vs/editor/common/languages/la import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { IndentConsts } from 'vs/editor/common/languages/supports/indentRules'; import * as indentUtils from 'vs/editor/contrib/indentation/browser/indentUtils'; -import { getGoodIndentForLine, getIndentMetadata, IIndentConverter } from 'vs/editor/common/languages/autoIndent'; +import { getGoodIndentForLine, getIndentMetadata, IIndentConverter, IVirtualModel } from 'vs/editor/common/languages/autoIndent'; import { getEnterAction } from 'vs/editor/common/languages/enterAction'; export class MoveLinesCommand implements ICommand { @@ -63,15 +63,17 @@ export class MoveLinesCommand implements ICommand { const { tabSize, indentSize, insertSpaces } = model.getOptions(); let indentConverter = this.buildIndentConverter(tabSize, indentSize, insertSpaces); - let virtualModel = { - getLineTokens: (lineNumber: number) => { - return model.getLineTokens(lineNumber); - }, - getLanguageId: () => { - return model.getLanguageId(); - }, - getLanguageIdAtPosition: (lineNumber: number, column: number) => { - return model.getLanguageIdAtPosition(lineNumber, column); + let virtualModel: IVirtualModel = { + tokenization: { + getLineTokens: (lineNumber: number) => { + return model.tokenization.getLineTokens(lineNumber); + }, + getLanguageId: () => { + return model.getLanguageId(); + }, + getLanguageIdAtPosition: (lineNumber: number, column: number) => { + return model.getLanguageIdAtPosition(lineNumber, column); + }, }, getLineContent: null as unknown as (lineNumber: number) => string, }; @@ -361,7 +363,7 @@ export class MoveLinesCommand implements ICommand { return false; } // if it's not easy to tokenize, we stop auto indent. - if (!model.isCheapToTokenize(selection.startLineNumber)) { + if (!model.tokenization.isCheapToTokenize(selection.startLineNumber)) { return false; } let languageAtSelectionStart = model.getLanguageIdAtPosition(selection.startLineNumber, 1); diff --git a/src/vs/editor/contrib/linesOperations/test/browser/moveLinesCommand.test.ts b/src/vs/editor/contrib/linesOperations/test/browser/moveLinesCommand.test.ts index 6ad8ce59833..af1d85e4d31 100644 --- a/src/vs/editor/contrib/linesOperations/test/browser/moveLinesCommand.test.ts +++ b/src/vs/editor/contrib/linesOperations/test/browser/moveLinesCommand.test.ts @@ -2,13 +2,15 @@ * 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 { EditorAutoIndentStrategy } from 'vs/editor/common/config/editorOptions'; import { Selection } from 'vs/editor/common/core/selection'; +import { ILanguageService } from 'vs/editor/common/languages/language'; import { IndentationRule } from 'vs/editor/common/languages/languageConfiguration'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { LanguageService } from 'vs/editor/common/services/languageService'; import { MoveLinesCommand } from 'vs/editor/contrib/linesOperations/browser/moveLinesCommand'; import { testCommand } from 'vs/editor/test/browser/testCommand'; -import { MockMode } from 'vs/editor/test/common/mocks/mockMode'; import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; function testMoveLinesDownCommand(lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection, languageConfigurationService = new TestLanguageConfigurationService()): void { @@ -259,13 +261,15 @@ suite('Editor Contrib - Move Lines Command', () => { }); }); -class IndentRulesMode extends MockMode { - private static readonly _id = 'moveLinesIndentMode'; +class IndentRulesMode extends Disposable { + public readonly languageId = 'moveLinesIndentMode'; constructor( indentationRules: IndentationRule, + @ILanguageService languageService: ILanguageService, @ILanguageConfigurationService languageConfigurationService: ILanguageConfigurationService ) { - super(IndentRulesMode._id); + super(); + this._register(languageService.registerLanguage({ id: this.languageId })); this._register(languageConfigurationService.register(this.languageId, { indentationRules: indentationRules })); @@ -282,8 +286,9 @@ suite('Editor contrib - Move Lines Command honors Indentation Rules', () => { // https://github.com/microsoft/vscode/issues/28552#issuecomment-307862797 test('first line indentation adjust to 0', () => { + const languageService = new LanguageService(); const languageConfigurationService = new TestLanguageConfigurationService(); - const mode = new IndentRulesMode(indentRules, languageConfigurationService); + const mode = new IndentRulesMode(indentRules, languageService, languageConfigurationService); testMoveLinesUpWithIndentCommand( mode.languageId, @@ -303,12 +308,14 @@ suite('Editor contrib - Move Lines Command honors Indentation Rules', () => { ); mode.dispose(); + languageService.dispose(); }); // https://github.com/microsoft/vscode/issues/28552#issuecomment-307867717 test('move lines across block', () => { + const languageService = new LanguageService(); const languageConfigurationService = new TestLanguageConfigurationService(); - const mode = new IndentRulesMode(indentRules, languageConfigurationService); + const mode = new IndentRulesMode(indentRules, languageService, languageConfigurationService); testMoveLinesDownWithIndentCommand( mode.languageId, @@ -334,6 +341,7 @@ suite('Editor contrib - Move Lines Command honors Indentation Rules', () => { ); mode.dispose(); + languageService.dispose(); }); @@ -360,12 +368,14 @@ suite('Editor contrib - Move Lines Command honors Indentation Rules', () => { }); }); -class EnterRulesMode extends MockMode { - private static readonly _id = 'moveLinesEnterMode'; +class EnterRulesMode extends Disposable { + public readonly languageId = 'moveLinesEnterMode'; constructor( + @ILanguageService languageService: ILanguageService, @ILanguageConfigurationService languageConfigurationService: ILanguageConfigurationService ) { - super(EnterRulesMode._id); + super(); + this._register(languageService.registerLanguage({ id: this.languageId })); this._register(languageConfigurationService.register(this.languageId, { indentationRules: { decreaseIndentPattern: /^\s*\[$/, @@ -381,8 +391,9 @@ class EnterRulesMode extends MockMode { suite('Editor - contrib - Move Lines Command honors onEnter Rules', () => { test('issue #54829. move block across block', () => { + const languageService = new LanguageService(); const languageConfigurationService = new TestLanguageConfigurationService(); - const mode = new EnterRulesMode(languageConfigurationService); + const mode = new EnterRulesMode(languageService, languageConfigurationService); testMoveLinesDownWithIndentCommand( mode.languageId, @@ -413,5 +424,6 @@ suite('Editor - contrib - Move Lines Command honors onEnter Rules', () => { ); mode.dispose(); + languageService.dispose(); }); }); diff --git a/src/vs/editor/contrib/markdownRenderer/browser/markdownRenderer.ts b/src/vs/editor/contrib/markdownRenderer/browser/markdownRenderer.ts index 2a323d76679..7220dbf1a4f 100644 --- a/src/vs/editor/contrib/markdownRenderer/browser/markdownRenderer.ts +++ b/src/vs/editor/contrib/markdownRenderer/browser/markdownRenderer.ts @@ -23,6 +23,7 @@ export interface IMarkdownRenderResult extends IDisposable { export interface IMarkdownRendererOptions { editor?: ICodeEditor; codeBlockFontFamily?: string; + codeBlockFontSize?: string; } /** @@ -93,6 +94,10 @@ export class MarkdownRenderer { element.style.fontFamily = this._options.codeBlockFontFamily; } + if (this._options.codeBlockFontSize !== undefined) { + element.style.fontSize = this._options.codeBlockFontSize; + } + return element; }, asyncRenderCallback: () => this._onDidRenderAsync.fire(), diff --git a/src/vs/editor/contrib/multicursor/browser/multicursor.ts b/src/vs/editor/contrib/multicursor/browser/multicursor.ts index 574a5be3e99..7c658f0fa9b 100644 --- a/src/vs/editor/contrib/multicursor/browser/multicursor.ts +++ b/src/vs/editor/contrib/multicursor/browser/multicursor.ts @@ -1095,7 +1095,7 @@ export class FocusNextCursor extends EditorAction { constructor() { super({ id: 'editor.action.focusNextCursor', - label: nls.localize('mutlicursor.focusNextCursor', "Focus next cursor"), + label: nls.localize('mutlicursor.focusNextCursor', "Focus Next Cursor"), description: { description: nls.localize('mutlicursor.focusNextCursor.description', "Focuses the next cursor"), args: [], @@ -1134,7 +1134,7 @@ export class FocusPreviousCursor extends EditorAction { constructor() { super({ id: 'editor.action.focusPreviousCursor', - label: nls.localize('mutlicursor.focusPreviousCursor', "Focus previous cursor"), + label: nls.localize('mutlicursor.focusPreviousCursor', "Focus Previous Cursor"), description: { description: nls.localize('mutlicursor.focusPreviousCursor.description', "Focuses the previous cursor"), args: [], diff --git a/src/vs/editor/contrib/parameterHints/browser/parameterHintsModel.ts b/src/vs/editor/contrib/parameterHints/browser/parameterHintsModel.ts index 67f90c816cd..5eff925127b 100644 --- a/src/vs/editor/contrib/parameterHints/browser/parameterHintsModel.ts +++ b/src/vs/editor/contrib/parameterHints/browser/parameterHintsModel.ts @@ -9,11 +9,11 @@ import { Emitter } from 'vs/base/common/event'; import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; -import { ICursorSelectionChangedEvent } from 'vs/editor/common/cursorEvents'; import { CharacterSet } from 'vs/editor/common/core/characterClassifier'; +import { ICursorSelectionChangedEvent } from 'vs/editor/common/cursorEvents'; +import { LanguageFeatureRegistry } from 'vs/editor/common/languageFeatureRegistry'; import * as languages from 'vs/editor/common/languages'; import { provideSignatureHelp } from 'vs/editor/contrib/parameterHints/browser/provideSignatureHelp'; -import { LanguageFeatureRegistry } from 'vs/editor/common/languageFeatureRegistry'; export interface TriggerContext { readonly triggerKind: languages.SignatureHelpTriggerKind; diff --git a/src/vs/editor/contrib/parameterHints/browser/parameterHintsWidget.ts b/src/vs/editor/contrib/parameterHints/browser/parameterHintsWidget.ts index ccf6e4ba11c..43aeb4c6509 100644 --- a/src/vs/editor/contrib/parameterHints/browser/parameterHintsWidget.ts +++ b/src/vs/editor/contrib/parameterHints/browser/parameterHintsWidget.ts @@ -13,21 +13,21 @@ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { escapeRegExpCharacters } from 'vs/base/common/strings'; import { assertIsDefined } from 'vs/base/common/types'; import 'vs/css!./parameterHints'; -import { IMarkdownRenderResult, MarkdownRenderer } from 'vs/editor/contrib/markdownRenderer/browser/markdownRenderer'; import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions'; import * as languages from 'vs/editor/common/languages'; import { ILanguageService } from 'vs/editor/common/languages/language'; +import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import { IMarkdownRenderResult, MarkdownRenderer } from 'vs/editor/contrib/markdownRenderer/browser/markdownRenderer'; import { ParameterHintsModel, TriggerContext } from 'vs/editor/contrib/parameterHints/browser/parameterHintsModel'; import { Context } from 'vs/editor/contrib/parameterHints/browser/provideSignatureHelp'; import * as nls from 'vs/nls'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { editorHoverBackground, editorHoverBorder, editorHoverForeground, registerColor, textCodeBlockBackground, textLinkActiveForeground, textLinkForeground, listHighlightForeground } from 'vs/platform/theme/common/colorRegistry'; +import { editorHoverBackground, editorHoverBorder, editorHoverForeground, listHighlightForeground, registerColor, textCodeBlockBackground, textLinkActiveForeground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; import { isHighContrast } from 'vs/platform/theme/common/theme'; import { registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; const $ = dom.$; diff --git a/src/vs/editor/contrib/parameterHints/browser/provideSignatureHelp.ts b/src/vs/editor/contrib/parameterHints/browser/provideSignatureHelp.ts index 81b24a508c4..8f4341b481f 100644 --- a/src/vs/editor/contrib/parameterHints/browser/provideSignatureHelp.ts +++ b/src/vs/editor/contrib/parameterHints/browser/provideSignatureHelp.ts @@ -8,13 +8,13 @@ import { onUnexpectedExternalError } from 'vs/base/common/errors'; import { assertType } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { IPosition, Position } from 'vs/editor/common/core/position'; -import { ITextModel } from 'vs/editor/common/model'; +import { LanguageFeatureRegistry } from 'vs/editor/common/languageFeatureRegistry'; import * as languages from 'vs/editor/common/languages'; +import { ITextModel } from 'vs/editor/common/model'; +import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { LanguageFeatureRegistry } from 'vs/editor/common/languageFeatureRegistry'; -import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; export const Context = { Visible: new RawContextKey<boolean>('parameterHintsVisible', false), diff --git a/src/vs/editor/contrib/parameterHints/test/browser/parameterHintsModel.test.ts b/src/vs/editor/contrib/parameterHints/test/browser/parameterHintsModel.test.ts index 3b85e657efc..0b52a5a7850 100644 --- a/src/vs/editor/contrib/parameterHints/test/browser/parameterHintsModel.test.ts +++ b/src/vs/editor/contrib/parameterHints/test/browser/parameterHintsModel.test.ts @@ -10,8 +10,9 @@ import { URI } from 'vs/base/common/uri'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; import { Position } from 'vs/editor/common/core/position'; import { Handler } from 'vs/editor/common/editorCommon'; -import { ITextModel } from 'vs/editor/common/model'; +import { LanguageFeatureRegistry } from 'vs/editor/common/languageFeatureRegistry'; import * as languages from 'vs/editor/common/languages'; +import { ITextModel } from 'vs/editor/common/model'; import { ParameterHintsModel } from 'vs/editor/contrib/parameterHints/browser/parameterHintsModel'; import { createTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { createTextModel } from 'vs/editor/test/common/testTextModel'; @@ -19,7 +20,6 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { InMemoryStorageService, IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; -import { LanguageFeatureRegistry } from 'vs/editor/common/languageFeatureRegistry'; const mockFile = URI.parse('test:somefile.ttt'); const mockFileSelector = { scheme: 'test' }; diff --git a/src/vs/editor/contrib/rename/browser/rename.ts b/src/vs/editor/contrib/rename/browser/rename.ts index 13291e31fb6..7c5d4a83c7e 100644 --- a/src/vs/editor/contrib/rename/browser/rename.ts +++ b/src/vs/editor/contrib/rename/browser/rename.ts @@ -132,7 +132,7 @@ class RenameController implements IEditorContribution { } private readonly _renameInputField: IdleValue<RenameInputField>; - private readonly _dispoableStore = new DisposableStore(); + private readonly _disposableStore = new DisposableStore(); private _cts: CancellationTokenSource = new CancellationTokenSource(); constructor( @@ -145,11 +145,11 @@ class RenameController implements IEditorContribution { @ITextResourceConfigurationService private readonly _configService: ITextResourceConfigurationService, @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, ) { - this._renameInputField = this._dispoableStore.add(new IdleValue(() => this._dispoableStore.add(this._instaService.createInstance(RenameInputField, this.editor, ['acceptRenameInput', 'acceptRenameInputWithPreview'])))); + this._renameInputField = this._disposableStore.add(new IdleValue(() => this._disposableStore.add(this._instaService.createInstance(RenameInputField, this.editor, ['acceptRenameInput', 'acceptRenameInputWithPreview'])))); } dispose(): void { - this._dispoableStore.dispose(); + this._disposableStore.dispose(); this._cts.dispose(true); } diff --git a/src/vs/editor/contrib/smartSelect/browser/bracketSelections.ts b/src/vs/editor/contrib/smartSelect/browser/bracketSelections.ts index 2f6b145de64..912181c48a4 100644 --- a/src/vs/editor/contrib/smartSelect/browser/bracketSelections.ts +++ b/src/vs/editor/contrib/smartSelect/browser/bracketSelections.ts @@ -51,12 +51,13 @@ export class BracketSelectionRangeProvider implements SelectionRangeProvider { setTimeout(() => BracketSelectionRangeProvider._bracketsRightYield(resolve, round + 1, model, pos, ranges)); break; } - const key = bracket.close[0]; - if (bracket.isOpen) { + if (bracket.bracketInfo.isOpeningBracket) { + const key = bracket.bracketInfo.bracketText; // wait for closing let val = counts.has(key) ? counts.get(key)! : 0; counts.set(key, val + 1); } else { + const key = bracket.bracketInfo.getClosedBrackets()[0].bracketText; // process closing let val = counts.has(key) ? counts.get(key)! : 0; val -= 1; @@ -96,12 +97,13 @@ export class BracketSelectionRangeProvider implements SelectionRangeProvider { setTimeout(() => BracketSelectionRangeProvider._bracketsLeftYield(resolve, round + 1, model, pos, ranges, bucket)); break; } - const key = bracket.close[0]; - if (!bracket.isOpen) { + if (!bracket.bracketInfo.isOpeningBracket) { + const key = bracket.bracketInfo.getClosedBrackets()[0].bracketText; // wait for opening let val = counts.has(key) ? counts.get(key)! : 0; counts.set(key, val + 1); } else { + const key = bracket.bracketInfo.bracketText; // opening let val = counts.has(key) ? counts.get(key)! : 0; val -= 1; diff --git a/src/vs/editor/contrib/smartSelect/test/browser/smartSelect.test.ts b/src/vs/editor/contrib/smartSelect/test/browser/smartSelect.test.ts index 3de4e65923c..462a79118df 100644 --- a/src/vs/editor/contrib/smartSelect/test/browser/smartSelect.test.ts +++ b/src/vs/editor/contrib/smartSelect/test/browser/smartSelect.test.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { Position } from 'vs/editor/common/core/position'; @@ -15,10 +16,14 @@ import { BracketSelectionRangeProvider } from 'vs/editor/contrib/smartSelect/bro import { provideSelectionRanges } from 'vs/editor/contrib/smartSelect/browser/smartSelect'; import { WordSelectionRangeProvider } from 'vs/editor/contrib/smartSelect/browser/wordSelections'; import { createModelServices } from 'vs/editor/test/common/testTextModel'; -import { StaticLanguageSelector } from 'vs/editor/test/common/mocks/mockMode'; import { javascriptOnEnterRules } from 'vs/editor/test/common/modes/supports/javascriptOnEnterRules'; import { LanguageFeatureRegistry } from 'vs/editor/common/languageFeatureRegistry'; -import { ILanguageService } from 'vs/editor/common/languages/language'; +import { ILanguageSelection, ILanguageService } from 'vs/editor/common/languages/language'; + +class StaticLanguageSelector implements ILanguageSelection { + readonly onDidChange: Event<string> = Event.None; + constructor(public readonly languageId: string) { } +} suite('SmartSelect', () => { diff --git a/src/vs/editor/contrib/snippet/browser/snippetController2.ts b/src/vs/editor/contrib/snippet/browser/snippetController2.ts index b1b4a6a1dd5..37b4a750c6d 100644 --- a/src/vs/editor/contrib/snippet/browser/snippetController2.ts +++ b/src/vs/editor/contrib/snippet/browser/snippetController2.ts @@ -167,7 +167,7 @@ export class SnippetController2 implements IEditorContribution { const registration = this._languageFeaturesService.completionProvider.register({ language: this._editor.getModel().getLanguageId(), - pattern: this._editor.getModel().uri.path, + pattern: this._editor.getModel().uri.fsPath, scheme: this._editor.getModel().uri.scheme }, this._choiceCompletionItemProvider); @@ -340,6 +340,7 @@ export function performSnippetEdit(editor: ICodeEditor, edit: SnippetTextEdit) { if (!controller) { return false; } + editor.focus(); editor.setSelection(edit.range); controller.insert(edit.snippet); return controller.isInSnippet(); diff --git a/src/vs/editor/contrib/snippet/browser/snippetParser.ts b/src/vs/editor/contrib/snippet/browser/snippetParser.ts index b896cff1f74..01baba9eeff 100644 --- a/src/vs/editor/contrib/snippet/browser/snippetParser.ts +++ b/src/vs/editor/contrib/snippet/browser/snippetParser.ts @@ -395,8 +395,7 @@ export class FormatString extends Marker { return value; } return match.map(word => { - return word.charAt(0).toUpperCase() - + word.substr(1).toLowerCase(); + return word.charAt(0).toUpperCase() + word.substr(1); }) .join(''); } @@ -408,11 +407,9 @@ export class FormatString extends Marker { } return match.map((word, index) => { if (index === 0) { - return word.toLowerCase(); - } else { - return word.charAt(0).toUpperCase() - + word.substr(1).toLowerCase(); + return word.charAt(0).toLowerCase() + word.substr(1); } + return word.charAt(0).toUpperCase() + word.substr(1); }) .join(''); } diff --git a/src/vs/editor/contrib/snippet/test/browser/snippetParser.test.ts b/src/vs/editor/contrib/snippet/test/browser/snippetParser.test.ts index 26cef12d7b3..320aa6af88e 100644 --- a/src/vs/editor/contrib/snippet/test/browser/snippetParser.test.ts +++ b/src/vs/editor/contrib/snippet/test/browser/snippetParser.test.ts @@ -656,8 +656,14 @@ suite('SnippetParser', () => { assert.strictEqual(new FormatString(1, 'capitalize').resolve('bar no repeat'), 'Bar no repeat'); assert.strictEqual(new FormatString(1, 'pascalcase').resolve('bar-foo'), 'BarFoo'); assert.strictEqual(new FormatString(1, 'pascalcase').resolve('bar-42-foo'), 'Bar42Foo'); + assert.strictEqual(new FormatString(1, 'pascalcase').resolve('snake_AndPascalCase'), 'SnakeAndPascalCase'); + assert.strictEqual(new FormatString(1, 'pascalcase').resolve('kebab-AndPascalCase'), 'KebabAndPascalCase'); + assert.strictEqual(new FormatString(1, 'pascalcase').resolve('_justPascalCase'), 'JustPascalCase'); assert.strictEqual(new FormatString(1, 'camelcase').resolve('bar-foo'), 'barFoo'); assert.strictEqual(new FormatString(1, 'camelcase').resolve('bar-42-foo'), 'bar42Foo'); + assert.strictEqual(new FormatString(1, 'camelcase').resolve('snake_AndCamelCase'), 'snakeAndCamelCase'); + assert.strictEqual(new FormatString(1, 'camelcase').resolve('kebab-AndCamelCase'), 'kebabAndCamelCase'); + assert.strictEqual(new FormatString(1, 'camelcase').resolve('_JustCamelCase'), 'justCamelCase'); assert.strictEqual(new FormatString(1, 'notKnown').resolve('input'), 'input'); // if diff --git a/src/vs/editor/contrib/suggest/browser/completionModel.ts b/src/vs/editor/contrib/suggest/browser/completionModel.ts index 73ddaa1a444..194f4a007f5 100644 --- a/src/vs/editor/contrib/suggest/browser/completionModel.ts +++ b/src/vs/editor/contrib/suggest/browser/completionModel.ts @@ -5,7 +5,7 @@ import { quickSelect } from 'vs/base/common/arrays'; import { CharCode } from 'vs/base/common/charCode'; -import { anyScore, fuzzyScore, FuzzyScore, fuzzyScoreGracefulAggressive, FuzzyScorer } from 'vs/base/common/filters'; +import { anyScore, fuzzyScore, FuzzyScore, fuzzyScoreGracefulAggressive, FuzzyScoreOptions, FuzzyScorer } from 'vs/base/common/filters'; import { compareIgnoreCase } from 'vs/base/common/strings'; import { InternalSuggestOptions } from 'vs/editor/common/config/editorOptions'; import { CompletionItemKind, CompletionItemProvider } from 'vs/editor/common/languages'; @@ -41,6 +41,7 @@ export class CompletionModel { private readonly _wordDistance: WordDistance; private readonly _options: InternalSuggestOptions; private readonly _snippetCompareFn = CompletionModel._compareCompletionItems; + private readonly _fuzzyScoreOptions: FuzzyScoreOptions; private _lineContext: LineContext; private _refilterKind: Refilter; @@ -55,7 +56,8 @@ export class CompletionModel { wordDistance: WordDistance, options: InternalSuggestOptions, snippetSuggestions: 'top' | 'bottom' | 'inline' | 'none', - readonly clipboardText: string | undefined + fuzzyScoreOptions: FuzzyScoreOptions | undefined = FuzzyScoreOptions.default, + readonly clipboardText: string | undefined = undefined ) { this._items = items; this._column = column; @@ -63,6 +65,7 @@ export class CompletionModel { this._options = options; this._refilterKind = Refilter.All; this._lineContext = lineContext; + this._fuzzyScoreOptions = fuzzyScoreOptions; if (snippetSuggestions === 'top') { this._snippetCompareFn = CompletionModel._compareCompletionItemsSnippetsUp; @@ -209,7 +212,7 @@ export class CompletionModel { // if it matches we check with the label to compute highlights // and if that doesn't yield a result we have no highlights, // despite having the match - let match = scoreFn(word, wordLow, wordPos, item.completion.filterText, item.filterTextLow!, 0, false); + let match = scoreFn(word, wordLow, wordPos, item.completion.filterText, item.filterTextLow!, 0, this._fuzzyScoreOptions); if (!match) { continue; // NO match } @@ -225,7 +228,7 @@ export class CompletionModel { } else { // by default match `word` against the `label` - let match = scoreFn(word, wordLow, wordPos, item.textLabel, item.labelLow, 0, false); + let match = scoreFn(word, wordLow, wordPos, item.textLabel, item.labelLow, 0, this._fuzzyScoreOptions); if (!match) { continue; // NO match } diff --git a/src/vs/editor/contrib/suggest/browser/suggest.ts b/src/vs/editor/contrib/suggest/browser/suggest.ts index 4a422eaef7d..d42c6a8b426 100644 --- a/src/vs/editor/contrib/suggest/browser/suggest.ts +++ b/src/vs/editor/contrib/suggest/browser/suggest.ts @@ -26,6 +26,7 @@ import { LanguageFeatureRegistry } from 'vs/editor/common/languageFeatureRegistr import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { historyNavigationVisible } from 'vs/platform/history/browser/contextScopedHistoryWidget'; import { InternalQuickSuggestionsOptions, QuickSuggestionsValue } from 'vs/editor/common/config/editorOptions'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; export const Context = { Visible: historyNavigationVisible, @@ -66,6 +67,9 @@ export class CompletionItem { idx?: number; word?: string; + // instrumentation + readonly extensionId?: ExtensionIdentifier; + // resolving private _isResolved?: boolean; private _resolveCache?: Promise<void>; @@ -89,6 +93,8 @@ export class CompletionItem { this.sortTextLow = completion.sortText && completion.sortText.toLowerCase(); this.filterTextLow = completion.filterText && completion.filterText.toLowerCase(); + this.extensionId = completion.extensionId; + // normalize ranges if (Range.isIRange(completion.range)) { this.editStart = new Position(completion.range.startLineNumber, completion.range.startColumn); @@ -397,12 +403,12 @@ CommandsRegistry.registerCommand('_executeCompletionItemProvider', async (access }); interface SuggestController extends IEditorContribution { - triggerSuggest(onlyFrom?: Set<languages.CompletionItemProvider>): void; + triggerSuggest(onlyFrom?: Set<languages.CompletionItemProvider>, auto?: boolean, noFilter?: boolean): void; } export function showSimpleSuggestions(editor: ICodeEditor, provider: languages.CompletionItemProvider) { editor.getContribution<SuggestController>('editor.contrib.suggestController')?.triggerSuggest( - new Set<languages.CompletionItemProvider>().add(provider) + new Set<languages.CompletionItemProvider>().add(provider), undefined, true ); } diff --git a/src/vs/editor/contrib/suggest/browser/suggestController.ts b/src/vs/editor/contrib/suggest/browser/suggestController.ts index 79266686612..883f01282f1 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestController.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestController.ts @@ -434,7 +434,7 @@ export class SuggestController implements IEditorContribution { // clear only now - after all tasks are done Promise.all(tasks).finally(() => { - this._reportSuggestionAcceptedTelemetry(model, event); + this._reportSuggestionAcceptedTelemetry(item, model, event); this.model.clear(); cts.dispose(); @@ -442,23 +442,25 @@ export class SuggestController implements IEditorContribution { } private _telemetryGate: number = 0; - private _reportSuggestionAcceptedTelemetry(model: ITextModel, acceptedSuggestion: ISelectedSuggestion) { + private _reportSuggestionAcceptedTelemetry(item: CompletionItem, model: ITextModel, acceptedSuggestion: ISelectedSuggestion) { if (this._telemetryGate++ % 100 !== 0) { return; } - type AcceptedSuggestion = { providerId: string; fileExtension: string; languageId: string; basenameHash: string }; + type AcceptedSuggestion = { providerId: string; fileExtension: string; languageId: string; basenameHash: string; kind: number }; type AcceptedSuggestionClassification = { providerId: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight' }; basenameHash: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight' }; fileExtension: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; languageId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; + kind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; }; // _debugDisplayName looks like `vscode.css-language-features(/-:)`, where the last bit is the trigger chars // normalize it to just the extension ID and lowercase - const providerId = (acceptedSuggestion.item.provider._debugDisplayName ?? 'unknown').split('(', 1)[0].toLowerCase(); + const providerId = item.extensionId ? item.extensionId.value : (acceptedSuggestion.item.provider._debugDisplayName ?? 'unknown').split('(', 1)[0].toLowerCase(); this._telemetryService.publicLog2<AcceptedSuggestion, AcceptedSuggestionClassification>('suggest.acceptedSuggestion', { providerId, + kind: item.completion.kind, basenameHash: hash(basename(model.uri)).toString(16), languageId: model.getLanguageId(), fileExtension: extname(model.uri), @@ -490,9 +492,9 @@ export class SuggestController implements IEditorContribution { } } - triggerSuggest(onlyFrom?: Set<CompletionItemProvider>, auto?: boolean): void { + triggerSuggest(onlyFrom?: Set<CompletionItemProvider>, auto?: boolean, noFilter?: boolean): void { if (this.editor.hasModel()) { - this.model.trigger({ auto: auto ?? false, shy: false }, false, onlyFrom); + this.model.trigger({ auto: auto ?? false, shy: false }, false, onlyFrom, undefined, noFilter); this.editor.revealPosition(this.editor.getPosition(), ScrollType.Smooth); this.editor.focus(); } diff --git a/src/vs/editor/contrib/suggest/browser/suggestInlineCompletions.ts b/src/vs/editor/contrib/suggest/browser/suggestInlineCompletions.ts index 40358156aac..42be4a075d1 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestInlineCompletions.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestInlineCompletions.ts @@ -5,26 +5,41 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { FuzzyScore } from 'vs/base/common/filters'; +import { Iterable } from 'vs/base/common/iterator'; import { IDisposable, RefCountedDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { EditorOption, FindComputedEditorOptionValueById } from 'vs/editor/common/config/editorOptions'; -import { Position } from 'vs/editor/common/core/position'; -import { Range } from 'vs/editor/common/core/range'; +import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; +import { IPosition, Position } from 'vs/editor/common/core/position'; +import { IRange, Range } from 'vs/editor/common/core/range'; import { IWordAtPosition } from 'vs/editor/common/core/wordHelper'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; -import { InlineCompletion, InlineCompletionContext, InlineCompletions, InlineCompletionsProvider } from 'vs/editor/common/languages'; +import { Command, CompletionItemProvider, CompletionTriggerKind, InlineCompletion, InlineCompletionContext, InlineCompletions, InlineCompletionsProvider } from 'vs/editor/common/languages'; import { ITextModel } from 'vs/editor/common/model'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { CompletionItemInsertTextRule } from 'vs/editor/common/standalone/standaloneEnums'; import { CompletionModel, LineContext } from 'vs/editor/contrib/suggest/browser/completionModel'; -import { CompletionItemModel, provideSuggestionItems, QuickSuggestionsOptions } from 'vs/editor/contrib/suggest/browser/suggest'; +import { CompletionItem, CompletionItemModel, CompletionOptions, provideSuggestionItems, QuickSuggestionsOptions } from 'vs/editor/contrib/suggest/browser/suggest'; +import { ISuggestMemoryService } from 'vs/editor/contrib/suggest/browser/suggestMemory'; import { WordDistance } from 'vs/editor/contrib/suggest/browser/wordDistance'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -class InlineCompletionResults extends RefCountedDisposable implements InlineCompletions { +class SuggestInlineCompletion implements InlineCompletion { + + constructor( + readonly range: IRange, + readonly insertText: string | { snippet: string }, + readonly filterText: string, + readonly additionalTextEdits: ISingleEditOperation[] | undefined, + readonly command: Command | undefined, + readonly completion: CompletionItem, + ) { } +} + +class InlineCompletionResults extends RefCountedDisposable implements InlineCompletions<SuggestInlineCompletion> { constructor( readonly model: ITextModel, @@ -32,19 +47,32 @@ class InlineCompletionResults extends RefCountedDisposable implements InlineComp readonly word: IWordAtPosition, readonly completionModel: CompletionModel, completions: CompletionItemModel, + @ISuggestMemoryService private readonly _suggestMemoryService: ISuggestMemoryService, ) { super(completions.disposable); } canBeReused(model: ITextModel, line: number, word: IWordAtPosition) { return this.model === model // same model - && this.line === line && this.word.startColumn === word.startColumn && this.word.endColumn < word.endColumn // same word + && this.line === line + && this.word.word.length > 0 + && this.word.startColumn === word.startColumn && this.word.endColumn < word.endColumn // same word && this.completionModel.incomplete.size === 0; // no incomplete results } - get items(): InlineCompletion[] { - const result: InlineCompletion[] = []; - for (const item of this.completionModel.items) { + get items(): SuggestInlineCompletion[] { + const result: SuggestInlineCompletion[] = []; + + // Split items by preselected index. This ensures the memory-selected item shows first and that better/worst + // ranked items are before/after + const { items } = this.completionModel; + const selectedIndex = this._suggestMemoryService.select(this.model, { lineNumber: this.line, column: this.word.endColumn + this.completionModel.lineContext.characterCountDelta }, items); + const first = Iterable.slice(items, selectedIndex); + const second = Iterable.slice(items, 0, selectedIndex); + + let resolveCount = 5; + + for (const item of Iterable.concat(first, second)) { if (item.score === FuzzyScore.Default) { // skip items that have no overlap @@ -59,20 +87,26 @@ class InlineCompletionResults extends RefCountedDisposable implements InlineComp ? { snippet: item.completion.insertText } : item.completion.insertText; - result.push({ + result.push(new SuggestInlineCompletion( range, - filterText: item.filterTextLow ?? item.labelLow, insertText, - command: item.completion.command, - additionalTextEdits: item.completion.additionalTextEdits - }); + item.filterTextLow ?? item.labelLow, + item.completion.additionalTextEdits, + item.completion.command, + item + )); + + // resolve the first N suggestions eagerly + if (resolveCount-- >= 0) { + item.resolve(CancellationToken.None); + } } return result; } } -class SuggestInlineCompletions implements InlineCompletionsProvider<InlineCompletionResults> { +export class SuggestInlineCompletions implements InlineCompletionsProvider<InlineCompletionResults> { private _lastResult?: InlineCompletionResults; @@ -80,6 +114,7 @@ class SuggestInlineCompletions implements InlineCompletionsProvider<InlineComple private readonly _getEditorOption: <T extends EditorOption>(id: T, model: ITextModel) => FindComputedEditorOptionValueById<T>, @ILanguageFeaturesService private readonly _languageFeatureService: ILanguageFeaturesService, @IClipboardService private readonly _clipboardService: IClipboardService, + @ISuggestMemoryService private readonly _suggestMemoryService: ISuggestMemoryService, ) { } async provideInlineCompletions(model: ITextModel, position: Position, context: InlineCompletionContext, token: CancellationToken): Promise<InlineCompletionResults | undefined> { @@ -94,23 +129,40 @@ class SuggestInlineCompletions implements InlineCompletionsProvider<InlineComple return; } - model.tokenizeIfCheap(position.lineNumber); - const lineTokens = model.getLineTokens(position.lineNumber); + model.tokenization.tokenizeIfCheap(position.lineNumber); + const lineTokens = model.tokenization.getLineTokens(position.lineNumber); const tokenType = lineTokens.getStandardTokenType(lineTokens.findTokenIndexAtOffset(Math.max(position.column - 1 - 1, 0))); if (QuickSuggestionsOptions.valueFor(config, tokenType) !== 'inline') { // quick suggest is off (for this token) return undefined; } - const wordInfo = model.getWordAtPosition(position); - if (!wordInfo || wordInfo.word.length === 0 || wordInfo.endColumn !== position.column) { - // not without true prefix, not inside word + // We consider non-empty leading words and trigger characters. The latter only + // when no word is being typed (word characters superseed trigger characters) + let wordInfo = model.getWordAtPosition(position); + let triggerCharacterInfo: { ch: string; providers: Set<CompletionItemProvider> } | undefined; + + if (!wordInfo?.word) { + triggerCharacterInfo = this._getTriggerCharacterInfo(model, position); + } + + if (!wordInfo?.word && !triggerCharacterInfo) { + // not at word, not a trigger character + return; + } + + // ensure that we have word information and that we are at the end of a word + // otherwise we stop because we don't want to do quick suggestions inside words + if (!wordInfo) { + wordInfo = model.getWordUntilPosition(position); + } + if (wordInfo.endColumn !== position.column) { return; } let result: InlineCompletionResults; const leadingLineContents = model.getValueInRange(new Range(position.lineNumber, 1, position.lineNumber, position.column)); - if (this._lastResult?.canBeReused(model, position.lineNumber, wordInfo)) { + if (!triggerCharacterInfo && this._lastResult?.canBeReused(model, position.lineNumber, wordInfo)) { // reuse a previous result iff possible, only a refilter is needed // TODO@jrieken this can be improved further and only incomplete results can be updated // console.log(`REUSE with ${wordInfo.word}`); @@ -124,8 +176,8 @@ class SuggestInlineCompletions implements InlineCompletionsProvider<InlineComple const completions = await provideSuggestionItems( this._languageFeatureService.completionProvider, model, position, - undefined, - undefined, + new CompletionOptions(undefined, undefined, triggerCharacterInfo?.providers), + triggerCharacterInfo && { triggerKind: CompletionTriggerKind.TriggerCharacter, triggerCharacter: triggerCharacterInfo.ch }, token ); @@ -141,18 +193,37 @@ class SuggestInlineCompletions implements InlineCompletionsProvider<InlineComple WordDistance.None, this._getEditorOption(EditorOption.suggest, model), this._getEditorOption(EditorOption.snippetSuggestions, model), + { boostFullMatch: false, firstMatchCanBeWeak: false }, clipboardText ); - result = new InlineCompletionResults(model, position.lineNumber, wordInfo, completionModel, completions); + result = new InlineCompletionResults(model, position.lineNumber, wordInfo, completionModel, completions, this._suggestMemoryService); } this._lastResult = result; return result; } + handleItemDidShow(_completions: InlineCompletionResults, item: SuggestInlineCompletion): void { + item.completion.resolve(CancellationToken.None); + } + freeInlineCompletions(result: InlineCompletionResults): void { result.release(); } + + private _getTriggerCharacterInfo(model: ITextModel, position: IPosition) { + const ch = model.getValueInRange(Range.fromPositions({ lineNumber: position.lineNumber, column: position.column - 1 }, position)); + const providers = new Set<CompletionItemProvider>(); + for (const provider of this._languageFeatureService.completionProvider.all(model)) { + if (provider.triggerCharacters?.includes(ch)) { + providers.add(provider); + } + } + if (providers.size === 0) { + return undefined; + } + return { providers, ch }; + } } class EditorContribution implements IEditorContribution { diff --git a/src/vs/editor/contrib/suggest/browser/suggestModel.ts b/src/vs/editor/contrib/suggest/browser/suggestModel.ts index f7a8da9b912..98c740be6f6 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestModel.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestModel.ts @@ -61,7 +61,7 @@ export class LineContext { } const model = editor.getModel(); const pos = editor.getPosition(); - model.tokenizeIfCheap(pos.lineNumber); + model.tokenization.tokenizeIfCheap(pos.lineNumber); const word = model.getWordAtPosition(pos); if (!word) { @@ -140,7 +140,6 @@ function canShowSuggestOnTriggerCharacters(editor: ICodeEditor, contextKeyServic export class SuggestModel implements IDisposable { private readonly _toDispose = new DisposableStore(); - private _quickSuggestDelay: number = 10; private readonly _triggerCharacterListener = new DisposableStore(); private readonly _triggerQuickSuggest = new TimeoutTimer(); private _state: State = State.Idle; @@ -182,7 +181,6 @@ export class SuggestModel implements IDisposable { })); this._toDispose.add(this._editor.onDidChangeConfiguration(() => { this._updateTriggerCharacters(); - this._updateQuickSuggest(); })); this._toDispose.add(this._languageFeaturesService.completionProvider.onDidChange(() => { this._updateTriggerCharacters(); @@ -213,7 +211,6 @@ export class SuggestModel implements IDisposable { })); this._updateTriggerCharacters(); - this._updateQuickSuggest(); } dispose(): void { @@ -224,16 +221,6 @@ export class SuggestModel implements IDisposable { this.cancel(); } - // --- handle configuration & precondition changes - - private _updateQuickSuggest(): void { - this._quickSuggestDelay = this._editor.getOption(EditorOption.quickSuggestionsDelay); - - if (isNaN(this._quickSuggestDelay) || (!this._quickSuggestDelay && this._quickSuggestDelay !== 0) || this._quickSuggestDelay < 0) { - this._quickSuggestDelay = 10; - } - } - private _updateTriggerCharacters(): void { this._triggerCharacterListener.clear(); @@ -395,7 +382,7 @@ export class SuggestModel implements IDisposable { if (!LineContext.shouldAutoTrigger(this._editor)) { return; } - if (!this._editor.hasModel()) { + if (!this._editor.hasModel() || !this._editor.hasWidgetFocus()) { return; } const model = this._editor.getModel(); @@ -408,8 +395,8 @@ export class SuggestModel implements IDisposable { if (!QuickSuggestionsOptions.isAllOn(config)) { // Check the type of the token that triggered this - model.tokenizeIfCheap(pos.lineNumber); - const lineTokens = model.getLineTokens(pos.lineNumber); + model.tokenization.tokenizeIfCheap(pos.lineNumber); + const lineTokens = model.tokenization.getLineTokens(pos.lineNumber); const tokenType = lineTokens.getStandardTokenType(lineTokens.findTokenIndexAtOffset(Math.max(pos.column - 1 - 1, 0))); if (QuickSuggestionsOptions.valueFor(config, tokenType) !== 'on') { return; @@ -428,7 +415,7 @@ export class SuggestModel implements IDisposable { // we made it till here -> trigger now this.trigger({ auto: true, shy: false }); - }, this._quickSuggestDelay); + }, this._editor.getOption(EditorOption.quickSuggestionsDelay)); } private _refilterCompletionItems(): void { @@ -451,7 +438,7 @@ export class SuggestModel implements IDisposable { }); } - trigger(context: SuggestTriggerContext, retrigger: boolean = false, onlyFrom?: Set<CompletionItemProvider>, existing?: { items: CompletionItem[]; clipboardText: string | undefined }): void { + trigger(context: SuggestTriggerContext, retrigger: boolean = false, onlyFrom?: Set<CompletionItemProvider>, existing?: { items: CompletionItem[]; clipboardText: string | undefined }, noFilter?: boolean): void { if (!this._editor.hasModel()) { return; } @@ -496,13 +483,14 @@ export class SuggestModel implements IDisposable { } const { itemKind: itemKindFilter, showDeprecated } = SuggestModel._createSuggestFilter(this._editor); + const completionOptions = new CompletionOptions(snippetSortOrder, !noFilter ? itemKindFilter : new Set(), onlyFrom, showDeprecated); const wordDistance = WordDistance.create(this._editorWorkerService, this._editor); const completions = provideSuggestionItems( this._languageFeaturesService.completionProvider, model, this._editor.getPosition(), - new CompletionOptions(snippetSortOrder, itemKindFilter, onlyFrom, showDeprecated), + completionOptions, suggestCtx, this._requestToken.token ); @@ -540,6 +528,7 @@ export class SuggestModel implements IDisposable { wordDistance, this._editor.getOption(EditorOption.suggest), this._editor.getOption(EditorOption.snippetSuggestions), + undefined, clipboardText ); @@ -564,7 +553,11 @@ export class SuggestModel implements IDisposable { setTimeout(() => { type Durations = { data: string }; - type DurationsClassification = { data: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' } }; + type DurationsClassification = { + owner: 'jrieken'; + comment: 'Completions performance numbers'; + data: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; + }; this._telemetryService.publicLog2<Durations, DurationsClassification>('suggest.durations.json', { data: JSON.stringify(durations) }); this._logService.debug('suggest.durations.json', durations); }); diff --git a/src/vs/editor/contrib/suggest/browser/suggestWidget.ts b/src/vs/editor/contrib/suggest/browser/suggestWidget.ts index 04e6390ba9c..8ac347d22b9 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestWidget.ts @@ -223,6 +223,7 @@ export class SuggestWidget implements IDisposable { alwaysConsumeMouseWheel: true, useShadows: false, mouseSupport: false, + multipleSelectionSupport: false, accessibilityProvider: { getRole: () => 'option', getWidgetAriaLabel: () => nls.localize('suggest', "Suggest"), diff --git a/src/vs/editor/contrib/suggest/test/browser/suggestInlineCompletions.test.ts b/src/vs/editor/contrib/suggest/test/browser/suggestInlineCompletions.test.ts new file mode 100644 index 00000000000..5fafaa7cee4 --- /dev/null +++ b/src/vs/editor/contrib/suggest/test/browser/suggestInlineCompletions.test.ts @@ -0,0 +1,89 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { Position } from 'vs/editor/common/core/position'; +import { CompletionContext, CompletionItem, CompletionItemKind, CompletionItemProvider, CompletionList, InlineCompletionTriggerKind, ProviderResult } from 'vs/editor/common/languages'; +import { createTextModel } from 'vs/editor/test/common/testTextModel'; +import { SuggestInlineCompletions } from 'vs/editor/contrib/suggest/browser/suggestInlineCompletions'; +import { createCodeEditorServices, instantiateTestCodeEditor, ITestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import { ITextModel } from 'vs/editor/common/model'; +import { Range } from 'vs/editor/common/core/range'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { ISuggestMemoryService } from 'vs/editor/contrib/suggest/browser/suggestMemory'; +import { mock } from 'vs/base/test/common/mock'; +import { TextModel } from 'vs/editor/common/model/textModel'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; + + +suite('Suggest Inline Completions', function () { + + const disposables = new DisposableStore(); + const services = new ServiceCollection([ISuggestMemoryService, new class extends mock<ISuggestMemoryService>() { + override select(): number { + return 0; + } + }]); + + let insta: TestInstantiationService; + let model: TextModel; + let editor: ITestCodeEditor; + + setup(function () { + + insta = createCodeEditorServices(disposables, services); + model = createTextModel('he', undefined, undefined, URI.from({ scheme: 'foo', path: 'foo.bar' })); + editor = instantiateTestCodeEditor(insta, model); + editor.updateOptions({ quickSuggestions: { comments: 'inline', strings: 'inline', other: 'inline' } }); + + insta.invokeFunction(accessor => { + accessor.get(ILanguageFeaturesService).completionProvider.register({ pattern: '*.bar', scheme: 'foo' }, new class implements CompletionItemProvider { + + triggerCharacters?: string[] | undefined; + + provideCompletionItems(model: ITextModel, position: Position, context: CompletionContext, token: CancellationToken): ProviderResult<CompletionList> { + + const word = model.getWordUntilPosition(position); + const range = new Range(position.lineNumber, word.startColumn, position.lineNumber, word.endColumn); + + const suggestions: CompletionItem[] = []; + suggestions.push({ insertText: 'hello', label: 'hello', range, kind: CompletionItemKind.Class }); + suggestions.push({ insertText: 'hell', label: 'hell', range, kind: CompletionItemKind.Class }); + suggestions.push({ insertText: 'hey', label: 'hey', range, kind: CompletionItemKind.Class }); + return { suggestions }; + } + + }); + }); + }); + + teardown(function () { + disposables.clear(); + model.dispose(); + editor.dispose(); + }); + + + test('Aggressive inline completions when typing within line #146948', async function () { + + const completions: SuggestInlineCompletions = insta.createInstance(SuggestInlineCompletions, (id: EditorOption) => editor.getOption(id)); + + { + // (1,3), end of word -> suggestions + const result = await completions.provideInlineCompletions(model, new Position(1, 3), { triggerKind: InlineCompletionTriggerKind.Explicit, selectedSuggestionInfo: undefined }, CancellationToken.None); + assert.strictEqual(result?.items.length, 3); + } + { + // (1,2), middle of word -> NO suggestions + const result = await completions.provideInlineCompletions(model, new Position(1, 2), { triggerKind: InlineCompletionTriggerKind.Explicit, selectedSuggestionInfo: undefined }, CancellationToken.None); + assert.ok(result === undefined); + } + }); +}); diff --git a/src/vs/editor/contrib/suggest/test/browser/suggestModel.test.ts b/src/vs/editor/contrib/suggest/test/browser/suggestModel.test.ts index 2e2864340dd..14069773008 100644 --- a/src/vs/editor/contrib/suggest/test/browser/suggestModel.test.ts +++ b/src/vs/editor/contrib/suggest/test/browser/suggestModel.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; import { Event } from 'vs/base/common/event'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { mock } from 'vs/base/test/common/mock'; import { CoreEditingCommands } from 'vs/editor/browser/coreCommands'; @@ -26,7 +26,6 @@ import { LineContext, SuggestModel } from 'vs/editor/contrib/suggest/browser/sug import { ISelectedSuggestion } from 'vs/editor/contrib/suggest/browser/suggestWidget'; import { createTestCodeEditor, ITestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { createModelServices, createTextModel, instantiateTextModel } from 'vs/editor/test/common/testTextModel'; -import { MockMode } from 'vs/editor/test/common/mocks/mockMode'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { MockKeybindingService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; @@ -61,6 +60,7 @@ function createMockEditor(model: TextModel, languageFeaturesService: ILanguageFe ), }); editor.registerAndInstantiateContribution(SnippetController2.ID, SnippetController2); + editor.hasWidgetFocus = () => true; return editor; } @@ -68,12 +68,14 @@ suite('SuggestModel - Context', function () { const OUTER_LANGUAGE_ID = 'outerMode'; const INNER_LANGUAGE_ID = 'innerMode'; - class OuterMode extends MockMode { + class OuterMode extends Disposable { + public readonly languageId = OUTER_LANGUAGE_ID; constructor( @ILanguageService languageService: ILanguageService, @ILanguageConfigurationService languageConfigurationService: ILanguageConfigurationService, ) { - super(OUTER_LANGUAGE_ID); + super(); + this._register(languageService.registerLanguage({ id: this.languageId })); this._register(languageConfigurationService.register(this.languageId, {})); this._register(TokenizationRegistry.register(this.languageId, { @@ -102,11 +104,14 @@ suite('SuggestModel - Context', function () { } } - class InnerMode extends MockMode { + class InnerMode extends Disposable { + public readonly languageId = INNER_LANGUAGE_ID; constructor( + @ILanguageService languageService: ILanguageService, @ILanguageConfigurationService languageConfigurationService: ILanguageConfigurationService ) { - super(INNER_LANGUAGE_ID); + super(); + this._register(languageService.registerLanguage({ id: this.languageId })); this._register(languageConfigurationService.register(this.languageId, {})); } } diff --git a/src/vs/editor/contrib/tokenization/browser/tokenization.ts b/src/vs/editor/contrib/tokenization/browser/tokenization.ts index 10cf6e1b1f6..8e673149542 100644 --- a/src/vs/editor/contrib/tokenization/browser/tokenization.ts +++ b/src/vs/editor/contrib/tokenization/browser/tokenization.ts @@ -23,9 +23,9 @@ class ForceRetokenizeAction extends EditorAction { return; } const model = editor.getModel(); - model.resetTokenization(); + model.tokenization.resetTokenization(); const sw = new StopWatch(true); - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); sw.stop(); console.log(`tokenization took ${sw.elapsed()}`); diff --git a/src/vs/editor/contrib/viewportSemanticTokens/browser/viewportSemanticTokens.ts b/src/vs/editor/contrib/viewportSemanticTokens/browser/viewportSemanticTokens.ts index a37ebe5408b..b3bb9805623 100644 --- a/src/vs/editor/contrib/viewportSemanticTokens/browser/viewportSemanticTokens.ts +++ b/src/vs/editor/contrib/viewportSemanticTokens/browser/viewportSemanticTokens.ts @@ -103,18 +103,18 @@ class ViewportSemanticTokensContribution extends Disposable implements IEditorCo return; } const model = this._editor.getModel(); - if (model.hasCompleteSemanticTokens()) { + if (model.tokenization.hasCompleteSemanticTokens()) { return; } if (!isSemanticColoringEnabled(model, this._themeService, this._configurationService)) { - if (model.hasSomeSemanticTokens()) { - model.setSemanticTokens(null, false); + if (model.tokenization.hasSomeSemanticTokens()) { + model.tokenization.setSemanticTokens(null, false); } return; } if (!hasDocumentRangeSemanticTokensProvider(this._provider, model)) { - if (model.hasSomeSemanticTokens()) { - model.setSemanticTokens(null, false); + if (model.tokenization.hasSomeSemanticTokens()) { + model.tokenization.setSemanticTokens(null, false); } return; } @@ -134,7 +134,7 @@ class ViewportSemanticTokensContribution extends Disposable implements IEditorCo } const { provider, tokens: result } = r; const styling = this._modelService.getSemanticTokensProviderStyling(provider); - model.setPartialSemanticTokens(range, toMultilineTokens2(result, styling, model.getLanguageId())); + model.tokenization.setPartialSemanticTokens(range, toMultilineTokens2(result, styling, model.getLanguageId())); }).then(() => this._removeOutstandingRequest(request), () => this._removeOutstandingRequest(request)); return request; } diff --git a/src/vs/editor/standalone/browser/colorizer.ts b/src/vs/editor/standalone/browser/colorizer.ts index e30d28fbebb..7adcf0cea76 100644 --- a/src/vs/editor/standalone/browser/colorizer.ts +++ b/src/vs/editor/standalone/browser/colorizer.ts @@ -99,8 +99,8 @@ export class Colorizer { public static colorizeModelLine(model: ITextModel, lineNumber: number, tabSize: number = 4): string { const content = model.getLineContent(lineNumber); - model.forceTokenization(lineNumber); - const tokens = model.getLineTokens(lineNumber); + model.tokenization.forceTokenization(lineNumber); + const tokens = model.tokenization.getLineTokens(lineNumber); const inflatedTokens = tokens.inflate(); return this.colorizeLine(content, model.mightContainNonBasicASCII(), model.mightContainRTL(), inflatedTokens, tabSize); } diff --git a/src/vs/editor/standalone/browser/standaloneLanguages.ts b/src/vs/editor/standalone/browser/standaloneLanguages.ts index ca8a3522845..f738b97ed24 100644 --- a/src/vs/editor/standalone/browser/standaloneLanguages.ts +++ b/src/vs/editor/standalone/browser/standaloneLanguages.ts @@ -28,6 +28,8 @@ import { LanguageSelector } from 'vs/editor/common/languageSelector'; * Register information about a new language. */ export function register(language: ILanguageExtensionPoint): void { + // Intentionally using the `ModesRegistry` here to avoid + // instantiating services too quickly in the standalone editor. ModesRegistry.registerLanguage(language); } diff --git a/src/vs/editor/standalone/browser/standaloneServices.ts b/src/vs/editor/standalone/browser/standaloneServices.ts index 3e53572dd10..81a9950b5e4 100644 --- a/src/vs/editor/standalone/browser/standaloneServices.ts +++ b/src/vs/editor/standalone/browser/standaloneServices.ts @@ -85,6 +85,7 @@ import { MarkerService } from 'vs/platform/markers/common/markerService'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IStorageService, InMemoryStorageService } from 'vs/platform/storage/common/storage'; +import { staticObservableValue } from 'vs/base/common/observableValue'; import 'vs/editor/common/services/languageFeaturesService'; @@ -640,7 +641,7 @@ class StandaloneResourcePropertiesService implements ITextResourcePropertiesServ class StandaloneTelemetryService implements ITelemetryService { declare readonly _serviceBrand: undefined; - public telemetryLevel = TelemetryLevel.NONE; + public telemetryLevel = staticObservableValue(TelemetryLevel.NONE); public sendErrorTelemetry = false; public setEnabled(value: boolean): void { diff --git a/src/vs/editor/standalone/browser/toggleHighContrast/toggleHighContrast.ts b/src/vs/editor/standalone/browser/toggleHighContrast/toggleHighContrast.ts index 3fce47976a9..d10e1ccafb9 100644 --- a/src/vs/editor/standalone/browser/toggleHighContrast/toggleHighContrast.ts +++ b/src/vs/editor/standalone/browser/toggleHighContrast/toggleHighContrast.ts @@ -7,6 +7,7 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction, ServicesAccessor, registerEditorAction } from 'vs/editor/browser/editorExtensions'; import { IStandaloneThemeService } from 'vs/editor/standalone/common/standaloneTheme'; import { ToggleHighContrastNLS } from 'vs/editor/common/standaloneStrings'; +import { isHighContrast } from 'vs/platform/theme/common/theme'; class ToggleHighContrast extends EditorAction { @@ -24,9 +25,9 @@ class ToggleHighContrast extends EditorAction { public run(accessor: ServicesAccessor, editor: ICodeEditor): void { const standaloneThemeService = accessor.get(IStandaloneThemeService); - if (this._originalThemeName) { + if (isHighContrast(standaloneThemeService.getColorTheme().type)) { // We must toggle back to the integrator's theme - standaloneThemeService.setTheme(this._originalThemeName); + standaloneThemeService.setTheme(this._originalThemeName || 'vs'); this._originalThemeName = null; } else { this._originalThemeName = standaloneThemeService.getColorTheme().themeName; diff --git a/src/vs/editor/standalone/common/themes.ts b/src/vs/editor/standalone/common/themes.ts index aeaefd1ae8f..e283e315d2c 100644 --- a/src/vs/editor/standalone/common/themes.ts +++ b/src/vs/editor/standalone/common/themes.ts @@ -215,12 +215,12 @@ export const hc_light: IStandaloneThemeData = { base: 'hc-light', inherit: false, rules: [ - { token: '', foreground: '000000', background: 'FFFFFF' }, - { token: 'invalid', foreground: 'cd3131' }, + { token: '', foreground: '292929', background: 'FFFFFF' }, + { token: 'invalid', foreground: 'B5200D' }, { token: 'emphasis', fontStyle: 'italic' }, { token: 'strong', fontStyle: 'bold' }, - { token: 'variable', foreground: '001188' }, + { token: 'variable', foreground: '264F70' }, { token: 'variable.predefined', foreground: '4864AA' }, { token: 'constant', foreground: 'dd0000' }, { token: 'comment', foreground: '008000' }, @@ -238,7 +238,7 @@ export const hc_light: IStandaloneThemeData = { { token: 'tag.class.pug', foreground: '4F76AC' }, { token: 'meta.scss', foreground: '800000' }, { token: 'metatag', foreground: 'e00000' }, - { token: 'metatag.content.html', foreground: 'FF0000' }, + { token: 'metatag.content.html', foreground: 'B5200D' }, { token: 'metatag.html', foreground: '808080' }, { token: 'metatag.xml', foreground: '808080' }, { token: 'metatag.php', fontStyle: 'bold' }, @@ -251,7 +251,7 @@ export const hc_light: IStandaloneThemeData = { { token: 'attribute.value', foreground: '0451A5' }, { token: 'string', foreground: 'A31515' }, - { token: 'string.sql', foreground: 'FF0000' }, + { token: 'string.sql', foreground: 'B5200D' }, { token: 'keyword', foreground: '0000FF' }, { token: 'keyword.flow', foreground: 'AF00DB' }, @@ -262,9 +262,9 @@ export const hc_light: IStandaloneThemeData = { ], colors: { [editorBackground]: '#FFFFFF', - [editorForeground]: '#000000', - [editorIndentGuides]: '#000000', - [editorActiveIndentGuides]: '#000000', + [editorForeground]: '#292929', + [editorIndentGuides]: '#292929', + [editorActiveIndentGuides]: '#292929', } }; /* -------------------------------- End hc-light theme -------------------------------- */ diff --git a/src/vs/editor/test/browser/commands/shiftCommand.test.ts b/src/vs/editor/test/browser/commands/shiftCommand.test.ts index 6f29362117e..0bf090ab53d 100644 --- a/src/vs/editor/test/browser/commands/shiftCommand.test.ts +++ b/src/vs/editor/test/browser/commands/shiftCommand.test.ts @@ -10,11 +10,13 @@ import { Selection } from 'vs/editor/common/core/selection'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { getEditOperation, testCommand } from 'vs/editor/test/browser/testCommand'; import { withEditorModel } from 'vs/editor/test/common/testTextModel'; -import { MockMode } from 'vs/editor/test/common/mocks/mockMode'; import { javascriptOnEnterRules } from 'vs/editor/test/common/modes/supports/javascriptOnEnterRules'; import { EditorAutoIndentStrategy } from 'vs/editor/common/config/editorOptions'; import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; /** * Create single edit operation @@ -27,14 +29,17 @@ export function createSingleEditOp(text: string, positionLineNumber: number, pos }; } -class DocBlockCommentMode extends MockMode { +class DocBlockCommentMode extends Disposable { - private static readonly _id = 'commentMode'; + public static languageId = 'commentMode'; + public readonly languageId = DocBlockCommentMode.languageId; constructor( + @ILanguageService languageService: ILanguageService, @ILanguageConfigurationService languageConfigurationService: ILanguageConfigurationService ) { - super(DocBlockCommentMode._id); + super(); + this._register(languageService.registerLanguage({ id: this.languageId })); this._register(languageConfigurationService.register(this.languageId, { brackets: [ ['(', ')'], @@ -47,7 +52,7 @@ class DocBlockCommentMode extends MockMode { } } -function testShiftCommand(lines: string[], languageId: string | null, useTabStops: boolean, selection: Selection, expectedLines: string[], expectedSelection: Selection, languageConfigurationService = new TestLanguageConfigurationService()): void { +function testShiftCommand(lines: string[], languageId: string | null, useTabStops: boolean, selection: Selection, expectedLines: string[], expectedSelection: Selection, prepare?: (accessor: ServicesAccessor, disposables: DisposableStore) => void): void { testCommand(lines, languageId, selection, (accessor, sel) => new ShiftCommand(sel, { isUnshift: false, tabSize: 4, @@ -55,10 +60,10 @@ function testShiftCommand(lines: string[], languageId: string | null, useTabStop insertSpaces: false, useTabStops: useTabStops, autoIndent: EditorAutoIndentStrategy.Full, - }, languageConfigurationService), expectedLines, expectedSelection); + }, accessor.get(ILanguageConfigurationService)), expectedLines, expectedSelection, undefined, prepare); } -function testUnshiftCommand(lines: string[], languageId: string | null, useTabStops: boolean, selection: Selection, expectedLines: string[], expectedSelection: Selection, languageConfigurationService = new TestLanguageConfigurationService()): void { +function testUnshiftCommand(lines: string[], languageId: string | null, useTabStops: boolean, selection: Selection, expectedLines: string[], expectedSelection: Selection, prepare?: (accessor: ServicesAccessor, disposables: DisposableStore) => void): void { testCommand(lines, languageId, selection, (accessor, sel) => new ShiftCommand(sel, { isUnshift: true, tabSize: 4, @@ -66,14 +71,13 @@ function testUnshiftCommand(lines: string[], languageId: string | null, useTabSt insertSpaces: false, useTabStops: useTabStops, autoIndent: EditorAutoIndentStrategy.Full, - }, languageConfigurationService), expectedLines, expectedSelection); + }, accessor.get(ILanguageConfigurationService)), expectedLines, expectedSelection, undefined, prepare); } -function withDockBlockCommentMode(callback: (mode: DocBlockCommentMode, languageConfigurationService: TestLanguageConfigurationService) => void): void { - const languageConfigurationService = new TestLanguageConfigurationService(); - let mode = new DocBlockCommentMode(languageConfigurationService); - callback(mode, languageConfigurationService); - mode.dispose(); +function prepareDocBlockCommentLanguage(accessor: ServicesAccessor, disposables: DisposableStore) { + const languageConfigurationService = accessor.get(ILanguageConfigurationService); + const languageService = accessor.get(ILanguageService); + disposables.add(new DocBlockCommentMode(languageService, languageConfigurationService)); } suite('Editor Commands - ShiftCommand', () => { @@ -558,105 +562,99 @@ suite('Editor Commands - ShiftCommand', () => { }); test('issue #348: indenting around doc block comments', () => { - withDockBlockCommentMode((mode, languageConfigurationService) => { - - testShiftCommand( - [ - '', - '/**', - ' * a doc comment', - ' */', - 'function hello() {}' - ], - mode.languageId, - true, - new Selection(1, 1, 5, 20), - [ - '', - '\t/**', - '\t * a doc comment', - '\t */', - '\tfunction hello() {}' - ], - new Selection(1, 1, 5, 21), - languageConfigurationService - ); - - testUnshiftCommand( - [ - '', - '/**', - ' * a doc comment', - ' */', - 'function hello() {}' - ], - mode.languageId, - true, - new Selection(1, 1, 5, 20), - [ - '', - '/**', - ' * a doc comment', - ' */', - 'function hello() {}' - ], - new Selection(1, 1, 5, 20), - languageConfigurationService - ); - - testUnshiftCommand( - [ - '\t', - '\t/**', - '\t * a doc comment', - '\t */', - '\tfunction hello() {}' - ], - mode.languageId, - true, - new Selection(1, 1, 5, 21), - [ - '', - '/**', - ' * a doc comment', - ' */', - 'function hello() {}' - ], - new Selection(1, 1, 5, 20), - languageConfigurationService - ); - - }); + testShiftCommand( + [ + '', + '/**', + ' * a doc comment', + ' */', + 'function hello() {}' + ], + DocBlockCommentMode.languageId, + true, + new Selection(1, 1, 5, 20), + [ + '', + '\t/**', + '\t * a doc comment', + '\t */', + '\tfunction hello() {}' + ], + new Selection(1, 1, 5, 21), + prepareDocBlockCommentLanguage + ); + + testUnshiftCommand( + [ + '', + '/**', + ' * a doc comment', + ' */', + 'function hello() {}' + ], + DocBlockCommentMode.languageId, + true, + new Selection(1, 1, 5, 20), + [ + '', + '/**', + ' * a doc comment', + ' */', + 'function hello() {}' + ], + new Selection(1, 1, 5, 20), + prepareDocBlockCommentLanguage + ); + + testUnshiftCommand( + [ + '\t', + '\t/**', + '\t * a doc comment', + '\t */', + '\tfunction hello() {}' + ], + DocBlockCommentMode.languageId, + true, + new Selection(1, 1, 5, 21), + [ + '', + '/**', + ' * a doc comment', + ' */', + 'function hello() {}' + ], + new Selection(1, 1, 5, 20), + prepareDocBlockCommentLanguage + ); }); test('issue #1609: Wrong indentation of block comments', () => { - withDockBlockCommentMode((mode, languageConfigurationService) => { - testShiftCommand( - [ - '', - '/**', - ' * test', - ' *', - ' * @type {number}', - ' */', - 'var foo = 0;' - ], - mode.languageId, - true, - new Selection(1, 1, 7, 13), - [ - '', - '\t/**', - '\t * test', - '\t *', - '\t * @type {number}', - '\t */', - '\tvar foo = 0;' - ], - new Selection(1, 1, 7, 14), - languageConfigurationService - ); - }); + testShiftCommand( + [ + '', + '/**', + ' * test', + ' *', + ' * @type {number}', + ' */', + 'var foo = 0;' + ], + DocBlockCommentMode.languageId, + true, + new Selection(1, 1, 7, 13), + [ + '', + '\t/**', + '\t * test', + '\t *', + '\t * @type {number}', + '\t */', + '\tvar foo = 0;' + ], + new Selection(1, 1, 7, 14), + prepareDocBlockCommentLanguage + ); }); test('issue #1620: a) Line indent doesn\'t handle leading whitespace properly', () => { diff --git a/src/vs/editor/test/browser/controller/cursor.test.ts b/src/vs/editor/test/browser/controller/cursor.test.ts index 2fad7ea8bea..a2d9d0f3cf9 100644 --- a/src/vs/editor/test/browser/controller/cursor.test.ts +++ b/src/vs/editor/test/browser/controller/cursor.test.ts @@ -2751,7 +2751,7 @@ suite('Editor Controller', () => { withTestCodeEditor(model, {}, (editor2, cursor2) => { editor1.onDidChangeCursorPosition(() => { - model.tokenizeIfCheap(1); + model.tokenization.tokenizeIfCheap(1); }); model.applyEdits([{ range: new Range(1, 1, 1, 1), text: '-' }]); @@ -3680,7 +3680,7 @@ suite('Editor Controller', () => { assertCursor(viewModel, new Selection(1, 12, 1, 12)); viewModel.type('\n', 'keyboard'); - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); assertCursor(viewModel, new Selection(2, 2, 2, 2)); moveTo(editor, viewModel, 3, 13, false); @@ -3743,7 +3743,7 @@ suite('Editor Controller', () => { assertCursor(viewModel, new Selection(2, 14, 2, 14)); viewModel.type('\n', 'keyboard'); - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); assertCursor(viewModel, new Selection(3, 1, 3, 1)); moveTo(editor, viewModel, 5, 16, false); @@ -3771,7 +3771,7 @@ suite('Editor Controller', () => { assertCursor(viewModel, new Selection(2, 11, 2, 11)); viewModel.type('\n', 'keyboard'); - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); assertCursor(viewModel, new Selection(3, 3, 3, 3)); viewModel.type('console.log();', 'keyboard'); @@ -3856,7 +3856,7 @@ suite('Editor Controller', () => { viewModel.type('\n', 'keyboard'); assertCursor(viewModel, new Selection(2, 5, 2, 5)); - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); moveTo(editor, viewModel, 3, 13, false); assertCursor(viewModel, new Selection(3, 13, 3, 13)); @@ -3878,7 +3878,7 @@ suite('Editor Controller', () => { assertCursor(viewModel, new Selection(1, 12, 1, 12)); viewModel.type('\n', 'keyboard'); - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); assertCursor(viewModel, new Selection(2, 5, 2, 5)); moveTo(editor, viewModel, 3, 16, false); @@ -3903,7 +3903,7 @@ suite('Editor Controller', () => { assertCursor(viewModel, new Selection(1, 12, 1, 12)); viewModel.type('\n', 'keyboard'); - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); assertCursor(viewModel, new Selection(2, 2, 2, 2)); moveTo(editor, viewModel, 3, 16, false); @@ -4551,8 +4551,8 @@ suite('Editor Controller', () => { ['(', ')'] ], indentationRules: { - increaseIndentPattern: new RegExp('^.*\\{[^}\"\\\']*$|^.*\\([^\\)\"\\\']*$|^\\s*(public|private|protected):\\s*$|^\\s*@(public|private|protected)\\s*$|^\\s*\\{\\}$'), - decreaseIndentPattern: new RegExp('^\\s*(\\s*/[*].*[*]/\\s*)*\\}|^\\s*(\\s*/[*].*[*]/\\s*)*\\)|^\\s*(public|private|protected):\\s*$|^\\s*@(public|private|protected)\\s*$'), + increaseIndentPattern: new RegExp("(^.*\\{[^}]*$)"), + decreaseIndentPattern: new RegExp("^\\s*\\}") } })); @@ -4614,13 +4614,13 @@ suite('Editor Controller', () => { assertCursor(viewModel, new Selection(1, 9, 1, 9)); viewModel.type('\n', 'keyboard'); - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); assertCursor(viewModel, new Selection(2, 2, 2, 2)); moveTo(editor, viewModel, 1, 9, false); assertCursor(viewModel, new Selection(1, 9, 1, 9)); viewModel.type('\n', 'keyboard'); - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); assertCursor(viewModel, new Selection(2, 2, 2, 2)); }); }); @@ -4931,7 +4931,7 @@ suite('Editor Controller', () => { text: ['const markup = highlight'], languageId: autoClosingLanguageId }, (editor, model, viewModel) => { - model.forceTokenization(1); + model.tokenization.forceTokenization(1); assertType(editor, model, viewModel, 1, 25, '`', '``', `auto closes \` @ (1, 25)`); }); }); @@ -4944,7 +4944,7 @@ suite('Editor Controller', () => { {}, (editor, viewModel) => { const model = viewModel.model; - model.forceTokenization(1); + model.tokenization.forceTokenization(1); assertType(editor, model, viewModel, 1, 28, '`', '`', `does not auto close \` @ (1, 28)`); } ); @@ -4980,7 +4980,7 @@ suite('Editor Controller', () => { const autoCloseColumns = extractAutoClosingSpecialColumns(model.getLineMaxColumn(lineNumber), autoClosePositions[i]); for (let column = 1; column < autoCloseColumns.length; column++) { - model.forceTokenization(lineNumber); + model.tokenization.forceTokenization(lineNumber); if (autoCloseColumns[column] === AutoClosingColumnType.Special1) { assertType(editor, model, viewModel, lineNumber, column, '(', '()', `auto closes @ (${lineNumber}, ${column})`); } else { @@ -5024,7 +5024,7 @@ suite('Editor Controller', () => { const autoCloseColumns = extractAutoClosingSpecialColumns(model.getLineMaxColumn(lineNumber), autoClosePositions[i]); for (let column = 1; column < autoCloseColumns.length; column++) { - model.forceTokenization(lineNumber); + model.tokenization.forceTokenization(lineNumber); if (autoCloseColumns[column] === AutoClosingColumnType.Special1) { assertType(editor, model, viewModel, lineNumber, column, '(', '()', `auto closes @ (${lineNumber}, ${column})`); } else { @@ -5055,7 +5055,7 @@ suite('Editor Controller', () => { const autoCloseColumns = extractAutoClosingSpecialColumns(model.getLineMaxColumn(lineNumber), autoClosePositions[i]); for (let column = 1; column < autoCloseColumns.length; column++) { - model.forceTokenization(lineNumber); + model.tokenization.forceTokenization(lineNumber); if (autoCloseColumns[column] === AutoClosingColumnType.Special1) { assertType(editor, model, viewModel, lineNumber, column, '(', '()', `auto closes @ (${lineNumber}, ${column})`); } else { @@ -5085,7 +5085,7 @@ suite('Editor Controller', () => { const autoCloseColumns = extractAutoClosingSpecialColumns(model.getLineMaxColumn(lineNumber), autoClosePositions[i]); for (let column = 1; column < autoCloseColumns.length; column++) { - model.forceTokenization(lineNumber); + model.tokenization.forceTokenization(lineNumber); if (autoCloseColumns[column] === AutoClosingColumnType.Special1) { assertType(editor, model, viewModel, lineNumber, column, '\'', '\'\'', `auto closes @ (${lineNumber}, ${column})`); } else { @@ -5131,7 +5131,7 @@ suite('Editor Controller', () => { const autoCloseColumns = extractAutoClosingSpecialColumns(model.getLineMaxColumn(lineNumber), autoClosePositions[i]); for (let column = 1; column < autoCloseColumns.length; column++) { - model.forceTokenization(lineNumber); + model.tokenization.forceTokenization(lineNumber); if (autoCloseColumns[column] === AutoClosingColumnType.Special1) { assertType(editor, model, viewModel, lineNumber, column, '(', '()', `auto closes @ (${lineNumber}, ${column})`); } else { @@ -5176,7 +5176,7 @@ suite('Editor Controller', () => { const autoCloseColumns = extractAutoClosingSpecialColumns(model.getLineMaxColumn(lineNumber), autoClosePositions[i]); for (let column = 1; column < autoCloseColumns.length; column++) { - model.forceTokenization(lineNumber); + model.tokenization.forceTokenization(lineNumber); if (autoCloseColumns[column] === AutoClosingColumnType.Special1) { assertType(editor, model, viewModel, lineNumber, column, '(', '()', `auto closes @ (${lineNumber}, ${column})`); assertType(editor, model, viewModel, lineNumber, column, '"', '""', `auto closes @ (${lineNumber}, ${column})`); @@ -5310,7 +5310,7 @@ suite('Editor Controller', () => { const autoCloseColumns = extractAutoClosingSpecialColumns(model.getLineMaxColumn(lineNumber), autoClosePositions[i]); for (let column = 1; column < autoCloseColumns.length; column++) { - model.forceTokenization(lineNumber); + model.tokenization.forceTokenization(lineNumber); if (autoCloseColumns[column] === AutoClosingColumnType.Special1) { assertType(editor, model, viewModel, lineNumber, column, '\'', '\'\'', `auto closes @ (${lineNumber}, ${column})`); } else if (autoCloseColumns[column] === AutoClosingColumnType.Special2) { @@ -5408,15 +5408,15 @@ suite('Editor Controller', () => { ], languageId: languageId }, (editor, model, viewModel) => { - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); assertType(editor, model, viewModel, 1, 4, '"', '"', `does not double quote when ending with open`); - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); assertType(editor, model, viewModel, 2, 4, '"', '"', `does not double quote when ending with open`); - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); assertType(editor, model, viewModel, 3, 4, '"', '"', `does not double quote when ending with open`); - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); assertType(editor, model, viewModel, 4, 2, '"', '"', `does not double quote when ending with open`); - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); assertType(editor, model, viewModel, 4, 3, '"', '"', `does not double quote when ending with open`); }); }); @@ -5447,50 +5447,50 @@ suite('Editor Controller', () => { } // First gif - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); typeCharacters(viewModel, 'teste1 = teste\' ok'); assert.strictEqual(model.getLineContent(1), 'teste1 = teste\' ok'); viewModel.setSelections('test', [new Selection(1, 1000, 1, 1000)]); typeCharacters(viewModel, '\n'); - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); typeCharacters(viewModel, 'teste2 = teste \'ok'); assert.strictEqual(model.getLineContent(2), 'teste2 = teste \'ok\''); viewModel.setSelections('test', [new Selection(2, 1000, 2, 1000)]); typeCharacters(viewModel, '\n'); - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); typeCharacters(viewModel, 'teste3 = teste" ok'); assert.strictEqual(model.getLineContent(3), 'teste3 = teste" ok'); viewModel.setSelections('test', [new Selection(3, 1000, 3, 1000)]); typeCharacters(viewModel, '\n'); - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); typeCharacters(viewModel, 'teste4 = teste "ok'); assert.strictEqual(model.getLineContent(4), 'teste4 = teste "ok"'); // Second gif viewModel.setSelections('test', [new Selection(4, 1000, 4, 1000)]); typeCharacters(viewModel, '\n'); - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); typeCharacters(viewModel, 'teste \''); assert.strictEqual(model.getLineContent(5), 'teste \'\''); viewModel.setSelections('test', [new Selection(5, 1000, 5, 1000)]); typeCharacters(viewModel, '\n'); - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); typeCharacters(viewModel, 'teste "'); assert.strictEqual(model.getLineContent(6), 'teste ""'); viewModel.setSelections('test', [new Selection(6, 1000, 6, 1000)]); typeCharacters(viewModel, '\n'); - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); typeCharacters(viewModel, 'teste\''); assert.strictEqual(model.getLineContent(7), 'teste\''); viewModel.setSelections('test', [new Selection(7, 1000, 7, 1000)]); typeCharacters(viewModel, '\n'); - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); typeCharacters(viewModel, 'teste"'); assert.strictEqual(model.getLineContent(8), 'teste"'); }); diff --git a/src/vs/editor/test/browser/testCommand.ts b/src/vs/editor/test/browser/testCommand.ts index 22d6a6fc9f2..d8349dd08d8 100644 --- a/src/vs/editor/test/browser/testCommand.ts +++ b/src/vs/editor/test/browser/testCommand.ts @@ -34,7 +34,7 @@ export function testCommand( const viewModel = editor.getViewModel()!; if (forceTokenization) { - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); } viewModel.setSelections('tests', [selection]); diff --git a/src/vs/editor/test/browser/view/minimapCharRenderer.test.ts b/src/vs/editor/test/browser/view/minimapCharRenderer.test.ts index c8d33c13f95..3359f968f5b 100644 --- a/src/vs/editor/test/browser/view/minimapCharRenderer.test.ts +++ b/src/vs/editor/test/browser/view/minimapCharRenderer.test.ts @@ -58,6 +58,7 @@ suite('MinimapCharRenderer', () => { function createFakeImageData(width: number, height: number): ImageData { return { + colorSpace: 'srgb', width: width, height: height, data: new Uint8ClampedArray(width * height * Constants.RGBA_CHANNELS_CNT) diff --git a/src/vs/editor/test/browser/viewModel/modelLineProjection.test.ts b/src/vs/editor/test/browser/viewModel/modelLineProjection.test.ts index 88c17e7823d..2fe1d779919 100644 --- a/src/vs/editor/test/browser/viewModel/modelLineProjection.test.ts +++ b/src/vs/editor/test/browser/viewModel/modelLineProjection.test.ts @@ -352,7 +352,7 @@ suite('SplitLinesCollection', () => { languageRegistration = languages.TokenizationRegistry.register(LANGUAGE_ID, tokenizationSupport); model = createTextModel(_text.join('\n'), LANGUAGE_ID); // force tokenization - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); }); teardown(() => { @@ -988,8 +988,10 @@ function createLineBreakData(breakingLengths: number[], breakingOffsetsVisibleCo function createModel(text: string): ISimpleModel { return { - getLineTokens: (lineNumber: number) => { - return null!; + tokenization: { + getLineTokens: (lineNumber: number) => { + return null!; + }, }, getLineContent: (lineNumber: number) => { return text; diff --git a/src/vs/editor/test/common/core/lineTokens.test.ts b/src/vs/editor/test/common/core/lineTokens.test.ts index ac74d86cf8f..f26d8930a56 100644 --- a/src/vs/editor/test/common/core/lineTokens.test.ts +++ b/src/vs/editor/test/common/core/lineTokens.test.ts @@ -57,7 +57,7 @@ suite('LineTokens', () => { test('withInserted 1', () => { const lineTokens = createTestLineTokens(); - assert.strictEqual(renderLineTokens(lineTokens), 'Hello (16384)world, (32768)this (49152)is (65536)a (81920)lovely (98304)day(114688)'); + assert.strictEqual(renderLineTokens(lineTokens), 'Hello (32768)world, (65536)this (98304)is (131072)a (163840)lovely (196608)day(229376)'); const lineTokens2 = lineTokens.withInserted([ { offset: 0, text: '1', tokenMetadata: 0, }, @@ -65,12 +65,12 @@ suite('LineTokens', () => { { offset: 9, text: '3', tokenMetadata: 0, }, ]); - assert.strictEqual(renderLineTokens(lineTokens2), '1(0)Hello (16384)2(0)wor(32768)3(0)ld, (32768)this (49152)is (65536)a (81920)lovely (98304)day(114688)'); + assert.strictEqual(renderLineTokens(lineTokens2), '1(0)Hello (32768)2(0)wor(65536)3(0)ld, (65536)this (98304)is (131072)a (163840)lovely (196608)day(229376)'); }); test('withInserted (tokens at the same position)', () => { const lineTokens = createTestLineTokens(); - assert.strictEqual(renderLineTokens(lineTokens), 'Hello (16384)world, (32768)this (49152)is (65536)a (81920)lovely (98304)day(114688)'); + assert.strictEqual(renderLineTokens(lineTokens), 'Hello (32768)world, (65536)this (98304)is (131072)a (163840)lovely (196608)day(229376)'); const lineTokens2 = lineTokens.withInserted([ { offset: 0, text: '1', tokenMetadata: 0, }, @@ -78,19 +78,19 @@ suite('LineTokens', () => { { offset: 0, text: '3', tokenMetadata: 0, }, ]); - assert.strictEqual(renderLineTokens(lineTokens2), '1(0)2(0)3(0)Hello (16384)world, (32768)this (49152)is (65536)a (81920)lovely (98304)day(114688)'); + assert.strictEqual(renderLineTokens(lineTokens2), '1(0)2(0)3(0)Hello (32768)world, (65536)this (98304)is (131072)a (163840)lovely (196608)day(229376)'); }); test('withInserted (tokens at the end)', () => { const lineTokens = createTestLineTokens(); - assert.strictEqual(renderLineTokens(lineTokens), 'Hello (16384)world, (32768)this (49152)is (65536)a (81920)lovely (98304)day(114688)'); + assert.strictEqual(renderLineTokens(lineTokens), 'Hello (32768)world, (65536)this (98304)is (131072)a (163840)lovely (196608)day(229376)'); const lineTokens2 = lineTokens.withInserted([ { offset: 'Hello world, this is a lovely day'.length - 1, text: '1', tokenMetadata: 0, }, { offset: 'Hello world, this is a lovely day'.length, text: '2', tokenMetadata: 0, }, ]); - assert.strictEqual(renderLineTokens(lineTokens2), 'Hello (16384)world, (32768)this (49152)is (65536)a (81920)lovely (98304)da(114688)1(0)y(114688)2(0)'); + assert.strictEqual(renderLineTokens(lineTokens2), 'Hello (32768)world, (65536)this (98304)is (131072)a (163840)lovely (196608)da(229376)1(0)y(229376)2(0)'); }); test('basics', () => { diff --git a/src/vs/editor/test/common/mocks/mockMode.ts b/src/vs/editor/test/common/mocks/mockMode.ts deleted file mode 100644 index 4ad2a686fbb..00000000000 --- a/src/vs/editor/test/common/mocks/mockMode.ts +++ /dev/null @@ -1,23 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Event } from 'vs/base/common/event'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { ModesRegistry } from 'vs/editor/common/languages/modesRegistry'; -import { ILanguageSelection } from 'vs/editor/common/languages/language'; - -export class MockMode extends Disposable { - constructor( - public readonly languageId: string - ) { - super(); - this._register(ModesRegistry.registerLanguage({ id: languageId })); - } -} - -export class StaticLanguageSelector implements ILanguageSelection { - readonly onDidChange: Event<string> = Event.None; - constructor(public readonly languageId: string) { } -} diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/tokenizer.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/tokenizer.test.ts index 6871809fb7b..c88eca52ef2 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/tokenizer.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/tokenizer.test.ts @@ -27,8 +27,8 @@ suite('Bracket Pair Colorizer - Tokenizer', () => { const denseKeyProvider = new DenseKeyProvider<string>(); - const tStandard = (text: string) => new TokenInfo(text, encodedMode1, StandardTokenType.Other); - const tComment = (text: string) => new TokenInfo(text, encodedMode1, StandardTokenType.Comment); + const tStandard = (text: string) => new TokenInfo(text, encodedMode1, StandardTokenType.Other, true); + const tComment = (text: string) => new TokenInfo(text, encodedMode1, StandardTokenType.Comment, true); const document = new TokenizedDocument([ tStandard(' { } '), tStandard('be'), tStandard('gin end'), tStandard('\n'), tStandard('hello'), tComment('{'), tStandard('}'), @@ -40,7 +40,7 @@ suite('Bracket Pair Colorizer - Tokenizer', () => { })); const model = disposableStore.add(instantiateTextModel(instantiationService, document.getText(), mode1)); - model.forceTokenization(model.getLineCount()); + model.tokenization.forceTokenization(model.getLineCount()); const brackets = new LanguageAgnosticBracketTokens(denseKeyProvider, l => languageConfigurationService.getLanguageConfiguration(l)); @@ -189,16 +189,23 @@ class TokenizedDocument { } class TokenInfo { - constructor(public readonly text: string, public readonly languageId: LanguageId, public readonly tokenType: StandardTokenType) { } + constructor( + public readonly text: string, + public readonly languageId: LanguageId, + public readonly tokenType: StandardTokenType, + public readonly hasBalancedBrackets: boolean, + ) { } getMetadata(): number { return ( - (this.languageId << MetadataConsts.LANGUAGEID_OFFSET) - | (this.tokenType << MetadataConsts.TOKEN_TYPE_OFFSET) - ) >>> 0; + (((this.languageId << MetadataConsts.LANGUAGEID_OFFSET) | + (this.tokenType << MetadataConsts.TOKEN_TYPE_OFFSET)) >>> + 0) | + (this.hasBalancedBrackets ? MetadataConsts.BALANCED_BRACKETS_MASK : 0) + ); } withText(text: string): TokenInfo { - return new TokenInfo(text, this.languageId, this.tokenType); + return new TokenInfo(text, this.languageId, this.tokenType, this.hasBalancedBrackets); } } diff --git a/src/vs/editor/test/common/model/model.line.test.ts b/src/vs/editor/test/common/model/model.line.test.ts index 541f39e0d39..7424e5f573e 100644 --- a/src/vs/editor/test/common/model/model.line.test.ts +++ b/src/vs/editor/test/common/model/model.line.test.ts @@ -125,7 +125,7 @@ suite('ModelLinesTokens', () => { for (let lineIndex = 0; lineIndex < expected.length; lineIndex++) { const actualLine = model.getLineContent(lineIndex + 1); - const actualTokens = model.getLineTokens(lineIndex + 1); + const actualTokens = model.tokenization.getLineTokens(lineIndex + 1); assert.strictEqual(actualLine, expected[lineIndex].text); assertLineTokens(actualTokens, expected[lineIndex].tokens); } @@ -462,7 +462,7 @@ suite('ModelLinesTokens', () => { text: 'a' }]); - const actualTokens = model.getLineTokens(1); + const actualTokens = model.tokenization.getLineTokens(1); assertLineTokens(actualTokens, [new TestToken(0, 1)]); model.dispose(); diff --git a/src/vs/editor/test/common/model/model.modes.test.ts b/src/vs/editor/test/common/model/model.modes.test.ts index 8f3bf8433ef..c6287828d6d 100644 --- a/src/vs/editor/test/common/model/model.modes.test.ts +++ b/src/vs/editor/test/common/model/model.modes.test.ts @@ -56,98 +56,98 @@ suite('Editor Model - Model Modes 1', () => { }); test('model calls syntax highlighter 1', () => { - thisModel.forceTokenization(1); + thisModel.tokenization.forceTokenization(1); checkAndClear(['1']); }); test('model calls syntax highlighter 2', () => { - thisModel.forceTokenization(2); + thisModel.tokenization.forceTokenization(2); checkAndClear(['1', '2']); - thisModel.forceTokenization(2); + thisModel.tokenization.forceTokenization(2); checkAndClear([]); }); test('model caches states', () => { - thisModel.forceTokenization(1); + thisModel.tokenization.forceTokenization(1); checkAndClear(['1']); - thisModel.forceTokenization(2); + thisModel.tokenization.forceTokenization(2); checkAndClear(['2']); - thisModel.forceTokenization(3); + thisModel.tokenization.forceTokenization(3); checkAndClear(['3']); - thisModel.forceTokenization(4); + thisModel.tokenization.forceTokenization(4); checkAndClear(['4']); - thisModel.forceTokenization(5); + thisModel.tokenization.forceTokenization(5); checkAndClear(['5']); - thisModel.forceTokenization(5); + thisModel.tokenization.forceTokenization(5); checkAndClear([]); }); test('model invalidates states for one line insert', () => { - thisModel.forceTokenization(5); + thisModel.tokenization.forceTokenization(5); checkAndClear(['1', '2', '3', '4', '5']); thisModel.applyEdits([EditOperation.insert(new Position(1, 1), '-')]); - thisModel.forceTokenization(5); + thisModel.tokenization.forceTokenization(5); checkAndClear(['-']); - thisModel.forceTokenization(5); + thisModel.tokenization.forceTokenization(5); checkAndClear([]); }); test('model invalidates states for many lines insert', () => { - thisModel.forceTokenization(5); + thisModel.tokenization.forceTokenization(5); checkAndClear(['1', '2', '3', '4', '5']); thisModel.applyEdits([EditOperation.insert(new Position(1, 1), '0\n-\n+')]); assert.strictEqual(thisModel.getLineCount(), 7); - thisModel.forceTokenization(7); + thisModel.tokenization.forceTokenization(7); checkAndClear(['0', '-', '+']); - thisModel.forceTokenization(7); + thisModel.tokenization.forceTokenization(7); checkAndClear([]); }); test('model invalidates states for one new line', () => { - thisModel.forceTokenization(5); + thisModel.tokenization.forceTokenization(5); checkAndClear(['1', '2', '3', '4', '5']); thisModel.applyEdits([EditOperation.insert(new Position(1, 2), '\n')]); thisModel.applyEdits([EditOperation.insert(new Position(2, 1), 'a')]); - thisModel.forceTokenization(6); + thisModel.tokenization.forceTokenization(6); checkAndClear(['1', 'a']); }); test('model invalidates states for one line delete', () => { - thisModel.forceTokenization(5); + thisModel.tokenization.forceTokenization(5); checkAndClear(['1', '2', '3', '4', '5']); thisModel.applyEdits([EditOperation.insert(new Position(1, 2), '-')]); - thisModel.forceTokenization(5); + thisModel.tokenization.forceTokenization(5); checkAndClear(['1']); thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 1, 2))]); - thisModel.forceTokenization(5); + thisModel.tokenization.forceTokenization(5); checkAndClear(['-']); - thisModel.forceTokenization(5); + thisModel.tokenization.forceTokenization(5); checkAndClear([]); }); test('model invalidates states for many lines delete', () => { - thisModel.forceTokenization(5); + thisModel.tokenization.forceTokenization(5); checkAndClear(['1', '2', '3', '4', '5']); thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 3, 1))]); - thisModel.forceTokenization(3); + thisModel.tokenization.forceTokenization(3); checkAndClear(['3']); - thisModel.forceTokenization(3); + thisModel.tokenization.forceTokenization(3); checkAndClear([]); }); }); @@ -208,55 +208,55 @@ suite('Editor Model - Model Modes 2', () => { }); test('getTokensForInvalidLines one text insert', () => { - thisModel.forceTokenization(5); + thisModel.tokenization.forceTokenization(5); checkAndClear(['Line1', 'Line2', 'Line3', 'Line4', 'Line5']); thisModel.applyEdits([EditOperation.insert(new Position(1, 6), '-')]); - thisModel.forceTokenization(5); + thisModel.tokenization.forceTokenization(5); checkAndClear(['Line1-', 'Line2']); }); test('getTokensForInvalidLines two text insert', () => { - thisModel.forceTokenization(5); + thisModel.tokenization.forceTokenization(5); checkAndClear(['Line1', 'Line2', 'Line3', 'Line4', 'Line5']); thisModel.applyEdits([ EditOperation.insert(new Position(1, 6), '-'), EditOperation.insert(new Position(3, 6), '-') ]); - thisModel.forceTokenization(5); + thisModel.tokenization.forceTokenization(5); checkAndClear(['Line1-', 'Line2', 'Line3-', 'Line4']); }); test('getTokensForInvalidLines one multi-line text insert, one small text insert', () => { - thisModel.forceTokenization(5); + thisModel.tokenization.forceTokenization(5); checkAndClear(['Line1', 'Line2', 'Line3', 'Line4', 'Line5']); thisModel.applyEdits([EditOperation.insert(new Position(1, 6), '\nNew line\nAnother new line')]); thisModel.applyEdits([EditOperation.insert(new Position(5, 6), '-')]); - thisModel.forceTokenization(7); + thisModel.tokenization.forceTokenization(7); checkAndClear(['Line1', 'New line', 'Another new line', 'Line2', 'Line3-', 'Line4']); }); test('getTokensForInvalidLines one delete text', () => { - thisModel.forceTokenization(5); + thisModel.tokenization.forceTokenization(5); checkAndClear(['Line1', 'Line2', 'Line3', 'Line4', 'Line5']); thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 1, 5))]); - thisModel.forceTokenization(5); + thisModel.tokenization.forceTokenization(5); checkAndClear(['1', 'Line2']); }); test('getTokensForInvalidLines one line delete text', () => { - thisModel.forceTokenization(5); + thisModel.tokenization.forceTokenization(5); checkAndClear(['Line1', 'Line2', 'Line3', 'Line4', 'Line5']); thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 2, 1))]); - thisModel.forceTokenization(4); + thisModel.tokenization.forceTokenization(4); checkAndClear(['Line2']); }); test('getTokensForInvalidLines multiple lines delete text', () => { - thisModel.forceTokenization(5); + thisModel.tokenization.forceTokenization(5); checkAndClear(['Line1', 'Line2', 'Line3', 'Line4', 'Line5']); thisModel.applyEdits([EditOperation.delete(new Range(1, 1, 3, 3))]); - thisModel.forceTokenization(3); + thisModel.tokenization.forceTokenization(3); checkAndClear(['ne3', 'Line4']); }); }); diff --git a/src/vs/editor/test/common/model/model.test.ts b/src/vs/editor/test/common/model/model.test.ts index 1b1f31881be..96fe698cb1b 100644 --- a/src/vs/editor/test/common/model/model.test.ts +++ b/src/vs/editor/test/common/model/model.test.ts @@ -13,7 +13,6 @@ import { InternalModelContentChangeEvent, ModelRawContentChangedEvent, ModelRawF import { EncodedTokenizationResult, IState, MetadataConsts, TokenizationRegistry } from 'vs/editor/common/languages'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { NullState } from 'vs/editor/common/languages/nullTokenize'; -import { MockMode } from 'vs/editor/test/common/mocks/mockMode'; import { createModelServices, createTextModel, instantiateTextModel } from 'vs/editor/test/common/testTextModel'; import { ILanguageService } from 'vs/editor/common/languages/language'; @@ -381,16 +380,19 @@ suite('Editor Model - Words', () => { const OUTER_LANGUAGE_ID = 'outerMode'; const INNER_LANGUAGE_ID = 'innerMode'; - class OuterMode extends MockMode { + class OuterMode extends Disposable { + + public readonly languageId = OUTER_LANGUAGE_ID; + constructor( @ILanguageService languageService: ILanguageService, @ILanguageConfigurationService languageConfigurationService: ILanguageConfigurationService ) { - super(OUTER_LANGUAGE_ID); - const languageIdCodec = languageService.languageIdCodec; - + super(); + this._register(languageService.registerLanguage({ id: this.languageId })); this._register(languageConfigurationService.register(this.languageId, {})); + const languageIdCodec = languageService.languageIdCodec; this._register(TokenizationRegistry.register(this.languageId, { getInitialState: (): IState => NullState, tokenize: undefined!, @@ -417,11 +419,16 @@ suite('Editor Model - Words', () => { } } - class InnerMode extends MockMode { + class InnerMode extends Disposable { + + public readonly languageId = INNER_LANGUAGE_ID; + constructor( + @ILanguageService languageService: ILanguageService, @ILanguageConfigurationService languageConfigurationService: ILanguageConfigurationService ) { - super(INNER_LANGUAGE_ID); + super(); + this._register(languageService.registerLanguage({ id: this.languageId })); this._register(languageConfigurationService.register(this.languageId, {})); } } diff --git a/src/vs/editor/test/common/model/textModelWithTokens.test.ts b/src/vs/editor/test/common/model/textModelWithTokens.test.ts index 59d3450eedb..ed66c94516d 100644 --- a/src/vs/editor/test/common/model/textModelWithTokens.test.ts +++ b/src/vs/editor/test/common/model/textModelWithTokens.test.ts @@ -31,17 +31,25 @@ function createTextModelWithBrackets(disposables: DisposableStore, text: string, } suite('TextModelWithTokens', () => { - function testBrackets(contents: string[], brackets: CharacterPair[]): void { + const languageId = 'testMode'; + const disposables = new DisposableStore(); + const instantiationService = createModelServices(disposables); + const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); + const languageService = instantiationService.get(ILanguageService); + disposables.add(languageService.registerLanguage({ id: languageId })); + disposables.add(languageConfigurationService.register(languageId, { + brackets: brackets + })); + + function toRelaxedFoundBracket(a: IFoundBracket | null) { if (!a) { return null; } return { range: a.range.toString(), - open: a.open[0], - close: a.close[0], - isOpen: a.isOpen + info: a.bracketInfo, }; } @@ -71,26 +79,13 @@ suite('TextModelWithTokens', () => { let ch = lineText.charAt(charIndex); if (charIsBracket[ch]) { expectedBrackets.push({ - open: [openForChar[ch]], - close: [closeForChar[ch]], - isOpen: charIsOpenBracket[ch], + bracketInfo: languageConfigurationService.getLanguageConfiguration(languageId).bracketsNew.getBracketInfo(ch)!, range: new Range(lineIndex + 1, charIndex + 1, lineIndex + 1, charIndex + 2) }); } } } - const languageId = 'testMode'; - const disposables = new DisposableStore(); - const instantiationService = createModelServices(disposables); - const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); - const languageService = instantiationService.get(ILanguageService); - - disposables.add(languageService.registerLanguage({ id: languageId })); - disposables.add(languageConfigurationService.register(languageId, { - brackets: brackets - })); - const model = disposables.add(instantiateTextModel(instantiationService, contents.join('\n'), languageId)); // findPrevBracket @@ -165,7 +160,11 @@ function assertIsNotBracket(model: TextModel, lineNumber: number, column: number } function assertIsBracket(model: TextModel, testPosition: Position, expected: [Range, Range]): void { + expected.sort(Range.compareRangesUsingStarts); const actual = model.bracketPairs.matchBracket(testPosition); + if (actual) { + actual.sort(Range.compareRangesUsingStarts); + } assert.deepStrictEqual(actual, expected, 'matches brackets at ' + testPosition); } @@ -273,7 +272,7 @@ suite('TextModelWithTokens - bracket matching', () => { }); }); -suite('TextModelWithTokens', () => { +suite('TextModelWithTokens 2', () => { test('bracket matching 3', () => { const text = [ @@ -360,10 +359,12 @@ suite('TextModelWithTokens', () => { const otherMetadata1 = ( (encodedMode1 << MetadataConsts.LANGUAGEID_OFFSET) | (StandardTokenType.Other << MetadataConsts.TOKEN_TYPE_OFFSET) + | (MetadataConsts.BALANCED_BRACKETS_MASK) ) >>> 0; const otherMetadata2 = ( (encodedMode2 << MetadataConsts.LANGUAGEID_OFFSET) | (StandardTokenType.Other << MetadataConsts.TOKEN_TYPE_OFFSET) + | (MetadataConsts.BALANCED_BRACKETS_MASK) ) >>> 0; const tokenizationSupport: ITokenizationSupport = { @@ -438,11 +439,14 @@ suite('TextModelWithTokens', () => { mode1 )); - model.forceTokenization(1); - model.forceTokenization(2); - model.forceTokenization(3); + model.tokenization.forceTokenization(1); + model.tokenization.forceTokenization(2); + model.tokenization.forceTokenization(3); - assert.deepStrictEqual(model.bracketPairs.matchBracket(new Position(2, 14)), [new Range(2, 13, 2, 14), new Range(2, 18, 2, 19)]); + assert.deepStrictEqual( + model.bracketPairs.matchBracket(new Position(2, 14)), + [new Range(2, 13, 2, 14), new Range(2, 18, 2, 19)] + ); disposables.dispose(); }); @@ -517,9 +521,9 @@ suite('TextModelWithTokens', () => { mode )); - model.forceTokenization(1); - model.forceTokenization(2); - model.forceTokenization(3); + model.tokenization.forceTokenization(1); + model.tokenization.forceTokenization(2); + model.tokenization.forceTokenization(3); assert.deepStrictEqual(model.bracketPairs.matchBracket(new Position(2, 23)), null); assert.deepStrictEqual(model.bracketPairs.matchBracket(new Position(2, 20)), null); @@ -534,9 +538,9 @@ suite('TextModelWithTokens regression tests', () => { test('microsoft/monaco-editor#122: Unhandled Exception: TypeError: Unable to get property \'replace\' of undefined or null reference', () => { function assertViewLineTokens(model: TextModel, lineNumber: number, forceTokenization: boolean, expected: TestLineToken[]): void { if (forceTokenization) { - model.forceTokenization(lineNumber); + model.tokenization.forceTokenization(lineNumber); } - let _actual = model.getLineTokens(lineNumber).inflate(); + let _actual = model.tokenization.getLineTokens(lineNumber).inflate(); interface ISimpleViewToken { endIndex: number; foreground: number; @@ -652,7 +656,7 @@ suite('TextModelWithTokens regression tests', () => { ); const actual = model.bracketPairs.matchBracket(new Position(3, 9)); - assert.deepStrictEqual(actual, [new Range(3, 6, 3, 17), new Range(2, 6, 2, 14)]); + assert.deepStrictEqual(actual, [new Range(2, 6, 2, 14), new Range(3, 6, 3, 17)]); disposables.dispose(); }); @@ -688,7 +692,7 @@ suite('TextModelWithTokens regression tests', () => { const model = disposables.add(instantiateTextModel(instantiationService, 'A model with one line', outerMode)); - model.forceTokenization(1); + model.tokenization.forceTokenization(1); assert.strictEqual(model.getLanguageIdAtPosition(1, 1), innerMode); disposables.dispose(); diff --git a/src/vs/editor/test/common/model/tokensStore.test.ts b/src/vs/editor/test/common/model/tokensStore.test.ts index ba69c425adf..0dcbea92aa7 100644 --- a/src/vs/editor/test/common/model/tokensStore.test.ts +++ b/src/vs/editor/test/common/model/tokensStore.test.ts @@ -7,12 +7,17 @@ import * as assert from 'assert'; import { SparseMultilineTokens } from 'vs/editor/common/tokens/sparseMultilineTokens'; import { SparseTokensStore } from 'vs/editor/common/tokens/sparseTokensStore'; import { Range } from 'vs/editor/common/core/range'; +import { Position } from 'vs/editor/common/core/position'; import { TextModel } from 'vs/editor/common/model/textModel'; import { MetadataConsts, TokenMetadata, FontStyle, ColorId } from 'vs/editor/common/languages'; -import { createTextModel } from 'vs/editor/test/common/testTextModel'; +import { createModelServices, createTextModel, instantiateTextModel } from 'vs/editor/test/common/testTextModel'; import { LineTokens } from 'vs/editor/common/tokens/lineTokens'; import { LanguageIdCodec } from 'vs/editor/common/services/languagesRegistry'; import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { ILanguageConfigurationService, LanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; suite('TokensStore', () => { @@ -74,7 +79,7 @@ suite('TokensStore', () => { function extractState(model: TextModel): string[] { let result: string[] = []; for (let lineNumber = 1; lineNumber <= model.getLineCount(); lineNumber++) { - const lineTokens = model.getLineTokens(lineNumber); + const lineTokens = model.tokenization.getLineTokens(lineNumber); const lineContent = model.getLineContent(lineNumber); let lineText = ''; @@ -101,7 +106,7 @@ suite('TokensStore', () => { function testTokensAdjustment(rawInitialState: string[], edits: ISingleEditOperation[], rawFinalState: string[]) { const initialState = parseTokensState(rawInitialState); const model = createTextModel(initialState.text); - model.setSemanticTokens([initialState.tokens], true); + model.tokenization.setSemanticTokens([initialState.tokens], true); model.applyEdits(edits); @@ -174,47 +179,64 @@ suite('TokensStore', () => { test('issue #91936: Semantic token color highlighting fails on line with selected text', () => { const model = createTextModel(' else if ($s = 08) then \'\\b\''); - model.setSemanticTokens([ + model.tokenization.setSemanticTokens([ SparseMultilineTokens.create(1, new Uint32Array([ - 0, 20, 24, 0b0111100000000010000, - 0, 25, 27, 0b0111100000000010000, - 0, 28, 29, 0b0000100000000010000, - 0, 29, 31, 0b1000000000000010000, - 0, 32, 33, 0b0000100000000010000, - 0, 34, 36, 0b0011000000000010000, - 0, 36, 37, 0b0000100000000010000, - 0, 38, 42, 0b0111100000000010000, - 0, 43, 47, 0b0101100000000010000, + 0, 20, 24, 0b01111000000000010000, + 0, 25, 27, 0b01111000000000010000, + 0, 28, 29, 0b00001000000000010000, + 0, 29, 31, 0b10000000000000010000, + 0, 32, 33, 0b00001000000000010000, + 0, 34, 36, 0b00110000000000010000, + 0, 36, 37, 0b00001000000000010000, + 0, 38, 42, 0b01111000000000010000, + 0, 43, 47, 0b01011000000000010000, ])) ], true); - const lineTokens = model.getLineTokens(1); + const lineTokens = model.tokenization.getLineTokens(1); let decodedTokens: number[] = []; for (let i = 0, len = lineTokens.getCount(); i < len; i++) { decodedTokens.push(lineTokens.getEndOffset(i), lineTokens.getMetadata(i)); } assert.deepStrictEqual(decodedTokens, [ - 20, 0b1000000000100000000000001, - 24, 0b1000000111100000000000001, - 25, 0b1000000000100000000000001, - 27, 0b1000000111100000000000001, - 28, 0b1000000000100000000000001, - 29, 0b1000000000100000000000001, - 31, 0b1000001000000000000000001, - 32, 0b1000000000100000000000001, - 33, 0b1000000000100000000000001, - 34, 0b1000000000100000000000001, - 36, 0b1000000011000000000000001, - 37, 0b1000000000100000000000001, - 38, 0b1000000000100000000000001, - 42, 0b1000000111100000000000001, - 43, 0b1000000000100000000000001, - 47, 0b1000000101100000000000001 + 20, 0b10000000001000010000000001, + 24, 0b10000001111000010000000001, + 25, 0b10000000001000010000000001, + 27, 0b10000001111000010000000001, + 28, 0b10000000001000010000000001, + 29, 0b10000000001000010000000001, + 31, 0b10000010000000010000000001, + 32, 0b10000000001000010000000001, + 33, 0b10000000001000010000000001, + 34, 0b10000000001000010000000001, + 36, 0b10000000110000010000000001, + 37, 0b10000000001000010000000001, + 38, 0b10000000001000010000000001, + 42, 0b10000001111000010000000001, + 43, 0b10000000001000010000000001, + 47, 0b10000001011000010000000001 ]); model.dispose(); }); + test('issue #147944: Language id "vs.editor.nullLanguage" is not configured nor known', () => { + const disposables = new DisposableStore(); + const instantiationService = createModelServices(disposables, new ServiceCollection([ + ILanguageConfigurationService, new SyncDescriptor(LanguageConfigurationService) + ])); + const model = instantiateTextModel(instantiationService, '--[[\n\n]]'); + model.tokenization.setSemanticTokens([ + SparseMultilineTokens.create(1, new Uint32Array([ + 0, 2, 4, 0b100000000000010000, + 1, 0, 0, 0b100000000000010000, + 2, 0, 2, 0b100000000000010000, + ])) + ], true); + assert.strictEqual(model.getWordAtPosition(new Position(2, 1)), null); + disposables.dispose(); + }); + test('partial tokens 1', () => { const codec = new LanguageIdCodec(); const store = new SparseTokensStore(codec); diff --git a/src/vs/editor/test/common/modes/languageSelector.test.ts b/src/vs/editor/test/common/modes/languageSelector.test.ts index e2d59071382..53cba5e386e 100644 --- a/src/vs/editor/test/common/modes/languageSelector.test.ts +++ b/src/vs/editor/test/common/modes/languageSelector.test.ts @@ -131,4 +131,23 @@ suite('LanguageSelector', function () { let value = score(selector, URI.file('/home/user/Desktop/test.json'), 'json', true, undefined); assert.strictEqual(value, 10); }); + + test('NotebookType without notebook', function () { + let obj = { + uri: URI.parse('file:///my/file.bat'), + langId: 'bat', + }; + + let value = score({ + language: 'bat', + notebookType: 'xxx' + }, obj.uri, obj.langId, true, undefined); + assert.strictEqual(value, 0); + + value = score({ + language: 'bat', + notebookType: '*' + }, obj.uri, obj.langId, true, undefined); + assert.strictEqual(value, 0); + }); }); diff --git a/src/vs/editor/test/common/modes/textToHtmlTokenizer.test.ts b/src/vs/editor/test/common/modes/textToHtmlTokenizer.test.ts index ffd333033f0..e49e06d804b 100644 --- a/src/vs/editor/test/common/modes/textToHtmlTokenizer.test.ts +++ b/src/vs/editor/test/common/modes/textToHtmlTokenizer.test.ts @@ -4,20 +4,36 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { EncodedTokenizationResult, ColorId, FontStyle, IState, MetadataConsts, TokenizationRegistry } from 'vs/editor/common/languages'; +import { ILanguageService } from 'vs/editor/common/languages/language'; import { tokenizeLineToHTML, _tokenizeToString } from 'vs/editor/common/languages/textToHtmlTokenizer'; import { LanguageIdCodec } from 'vs/editor/common/services/languagesRegistry'; import { TestLineToken, TestLineTokens } from 'vs/editor/test/common/core/testLineToken'; -import { MockMode } from 'vs/editor/test/common/mocks/mockMode'; +import { createModelServices } from 'vs/editor/test/common/testTextModel'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; suite('Editor Modes - textToHtmlTokenizer', () => { + + let disposables: DisposableStore; + let instantiationService: TestInstantiationService; + + setup(() => { + disposables = new DisposableStore(); + instantiationService = createModelServices(disposables); + }); + + teardown(() => { + disposables.dispose(); + }); + function toStr(pieces: { className: string; text: string }[]): string { let resultArr = pieces.map((t) => `<span class="${t.className}">${t.text}</span>`); return resultArr.join(''); } test('TextToHtmlTokenizer 1', () => { - let mode = new Mode(); + const mode = disposables.add(instantiationService.createInstance(Mode)); let support = TokenizationRegistry.get(mode.languageId)!; let actual = _tokenizeToString('.abc..def...gh', new LanguageIdCodec(), support); @@ -32,12 +48,10 @@ suite('Editor Modes - textToHtmlTokenizer', () => { let expectedStr = `<div class="monaco-tokenized-source">${toStr(expected)}</div>`; assert.strictEqual(actual, expectedStr); - - mode.dispose(); }); test('TextToHtmlTokenizer 2', () => { - let mode = new Mode(); + const mode = disposables.add(instantiationService.createInstance(Mode)); let support = TokenizationRegistry.get(mode.languageId)!; let actual = _tokenizeToString('.abc..def...gh\n.abc..def...gh', new LanguageIdCodec(), support); @@ -62,8 +76,6 @@ suite('Editor Modes - textToHtmlTokenizer', () => { let expectedStr = `<div class="monaco-tokenized-source">${expectedStr1}<br/>${expectedStr2}</div>`; assert.strictEqual(actual, expectedStr); - - mode.dispose(); }); test('tokenizeLineToHTML', () => { @@ -278,12 +290,15 @@ suite('Editor Modes - textToHtmlTokenizer', () => { }); -class Mode extends MockMode { +class Mode extends Disposable { - private static readonly _id = 'textToHtmlTokenizerMode'; + private readonly languageId = 'textToHtmlTokenizerMode'; - constructor() { - super(Mode._id); + constructor( + @ILanguageService languageService: ILanguageService + ) { + super(); + this._register(languageService.registerLanguage({ id: this.languageId })); this._register(TokenizationRegistry.register(this.languageId, { getInitialState: (): IState => null!, tokenize: undefined!, diff --git a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts index 8dd03f9c013..4264f60e47e 100644 --- a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts +++ b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts @@ -465,8 +465,8 @@ suite('viewLineRenderer.renderLine', () => { const expectedOutput = [ '<span class="mtk6">var</span>', - '<span dir="auto" class="mtk1">\u00a0קודמות\u00a0=\u00a0</span>', - '<span dir="auto" class="mtk20">"מיותר\u00a0קודמות\u00a0צ\'ט\u00a0של,\u00a0אם\u00a0לשון\u00a0העברית\u00a0שינויים\u00a0ויש,\u00a0אם"</span>', + '<span style="unicode-bidi:isolate" class="mtk1">\u00a0קודמות\u00a0=\u00a0</span>', + '<span style="unicode-bidi:isolate" class="mtk20">"מיותר\u00a0קודמות\u00a0צ\'ט\u00a0של,\u00a0אם\u00a0לשון\u00a0העברית\u00a0שינויים\u00a0ויש,\u00a0אם"</span>', '<span class="mtk1">;</span>' ].join(''); @@ -519,9 +519,9 @@ suite('viewLineRenderer.renderLine', () => { '<span class="mtk4">\u00a0</span>', '<span class="mtk5">value</span>', '<span class="mtk4">=</span>', - '<span dir="auto" class="mtk6">"العربية"</span>', + '<span style="unicode-bidi:isolate" class="mtk6">"العربية"</span>', '<span class="mtk2">></span>', - '<span dir="auto" class="mtk4">العربية</span>', + '<span style="unicode-bidi:isolate" class="mtk4">العربية</span>', '<span class="mtk2"></</span>', '<span class="mtk3">option</span>', '<span class="mtk2">></span>', @@ -553,6 +553,52 @@ suite('viewLineRenderer.renderLine', () => { assert.strictEqual(_actual.containsRTL, true); }); + test('issue #99589: Rendering whitespace influences bidi layout', () => { + const lineText = ' [\"🖨️ چاپ فاکتور\",\"🎨 تنظیمات\"]'; + + const lineParts = createViewLineTokens([ + createPart(5, 2), + createPart(21, 3), + createPart(22, 2), + createPart(34, 3), + createPart(35, 2), + ]); + + const expectedOutput = [ + '<span class="mtkw">\u00b7\u00b7\u00b7\u00b7</span>', + '<span class="mtk2">[</span>', + '<span style="unicode-bidi:isolate" class="mtk3">"🖨️\u00a0چاپ\u00a0فاکتور"</span>', + '<span class="mtk2">,</span>', + '<span style="unicode-bidi:isolate" class="mtk3">"🎨\u00a0تنظیمات"</span>', + '<span class="mtk2">]</span>' + ].join(''); + + const _actual = renderViewLine(new RenderLineInput( + true, + true, + lineText, + false, + false, + true, + 0, + lineParts, + [], + 4, + 0, + 10, + 10, + 10, + -1, + 'all', + false, + false, + null + )); + + assert.strictEqual(_actual.html, '<span dir="ltr">' + expectedOutput + '</span>'); + assert.strictEqual(_actual.containsRTL, true); + }); + test('issue #6885: Splits large tokens', () => { // 1 1 1 // 1 2 3 4 5 6 7 8 9 0 1 2 @@ -738,7 +784,7 @@ suite('viewLineRenderer.renderLine', () => { const lineText = 'את גרמנית בהתייחסות שמו, שנתי המשפט אל חפש, אם כתב אחרים ולחבר. של התוכן אודות בויקיפדיה כלל, של עזרה כימיה היא. על עמוד יוצרים מיתולוגיה סדר, אם שכל שתפו לעברית שינויים, אם שאלות אנגלית עזה. שמות בקלות מה סדר.'; const lineParts = createViewLineTokens([createPart(lineText.length, 1)]); const expectedOutput = [ - '<span dir="auto" class="mtk1">את\u00a0גרמנית\u00a0בהתייחסות\u00a0שמו,\u00a0שנתי\u00a0המשפט\u00a0אל\u00a0חפש,\u00a0אם\u00a0כתב\u00a0אחרים\u00a0ולחבר.\u00a0של\u00a0התוכן\u00a0אודות\u00a0בויקיפדיה\u00a0כלל,\u00a0של\u00a0עזרה\u00a0כימיה\u00a0היא.\u00a0על\u00a0עמוד\u00a0יוצרים\u00a0מיתולוגיה\u00a0סדר,\u00a0אם\u00a0שכל\u00a0שתפו\u00a0לעברית\u00a0שינויים,\u00a0אם\u00a0שאלות\u00a0אנגלית\u00a0עזה.\u00a0שמות\u00a0בקלות\u00a0מה\u00a0סדר.</span>' + '<span style="unicode-bidi:isolate" class="mtk1">את\u00a0גרמנית\u00a0בהתייחסות\u00a0שמו,\u00a0שנתי\u00a0המשפט\u00a0אל\u00a0חפש,\u00a0אם\u00a0כתב\u00a0אחרים\u00a0ולחבר.\u00a0של\u00a0התוכן\u00a0אודות\u00a0בויקיפדיה\u00a0כלל,\u00a0של\u00a0עזרה\u00a0כימיה\u00a0היא.\u00a0על\u00a0עמוד\u00a0יוצרים\u00a0מיתולוגיה\u00a0סדר,\u00a0אם\u00a0שכל\u00a0שתפו\u00a0לעברית\u00a0שינויים,\u00a0אם\u00a0שאלות\u00a0אנגלית\u00a0עזה.\u00a0שמות\u00a0בקלות\u00a0מה\u00a0סדר.</span>' ]; const actual = renderViewLine(new RenderLineInput( false, diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 4ce9d88a0d8..06cd6cc6e94 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -3721,7 +3721,7 @@ declare namespace monaco.editor { * Enable the inline hints. * Defaults to true. */ - enabled?: boolean; + enabled?: 'on' | 'off' | 'offUnlessPressed' | 'onUnlessPressed'; /** * Font size of inline hints. * Default to 90% of the editor font size. @@ -4028,7 +4028,7 @@ declare namespace monaco.editor { * Enable highlighting of the active indent guide. * Defaults to true. */ - highlightActiveIndentation?: boolean; + highlightActiveIndentation?: boolean | 'always'; } /** @@ -6328,6 +6328,10 @@ declare namespace monaco.languages { export interface InlineCompletions<TItem extends InlineCompletion = InlineCompletion> { readonly items: readonly TItem[]; + /** + * A list of commands associated with the inline completions of this list. + */ + readonly commands?: Command[]; } export interface InlineCompletionsProvider<T extends InlineCompletions = InlineCompletions> { diff --git a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts index 5c65a7784e7..29290a17a10 100644 --- a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts +++ b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts @@ -123,6 +123,7 @@ function fillInActions( export interface IMenuEntryActionViewItemOptions { draggable?: boolean; + keybinding?: string; } export class MenuEntryActionViewItem extends ActionViewItem { @@ -138,7 +139,7 @@ export class MenuEntryActionViewItem extends ActionViewItem { @INotificationService protected _notificationService: INotificationService, @IContextKeyService protected _contextKeyService: IContextKeyService ) { - super(undefined, _action, { icon: !!(_action.class || _action.item.icon), label: !_action.class && !_action.item.icon, draggable: options?.draggable }); + super(undefined, _action, { icon: !!(_action.class || _action.item.icon), label: !_action.class && !_action.item.icon, draggable: options?.draggable, keybinding: options?.keybinding }); this._altKey = ModifierKeyEmitter.getInstance(); } @@ -172,7 +173,7 @@ export class MenuEntryActionViewItem extends ActionViewItem { let alternativeKeyDown = this._altKey.keyStatus.altKey || ((isWindows || isLinux) && this._altKey.keyStatus.shiftKey); const updateAltState = () => { - const wantsAltCommand = mouseOver && alternativeKeyDown; + const wantsAltCommand = mouseOver && alternativeKeyDown && !!this._commandAction.alt?.enabled; if (wantsAltCommand !== this._wantsAltCommand) { this._wantsAltCommand = wantsAltCommand; this.updateLabel(); @@ -214,7 +215,7 @@ export class MenuEntryActionViewItem extends ActionViewItem { let title = keybindingLabel ? localize('titleAndKb', "{0} ({1})", tooltip, keybindingLabel) : tooltip; - if (!this._wantsAltCommand && this._menuItemAction.alt) { + if (!this._wantsAltCommand && this._menuItemAction.alt?.enabled) { const altTooltip = this._menuItemAction.alt.tooltip || this._menuItemAction.alt.label; const altKeybinding = this._keybindingService.lookupKeybinding(this._menuItemAction.alt.id, this._contextKeyService); const altKeybindingLabel = altKeybinding && altKeybinding.getLabel(); @@ -224,6 +225,7 @@ export class MenuEntryActionViewItem extends ActionViewItem { title += `\n[${UILabelProvider.modifierLabels[OS].altKey}] ${altTitleSection}`; } this.label.title = title; + this.label.setAttribute('aria-label', title); } } @@ -312,7 +314,12 @@ export class SubmenuEntryActionViewItem extends DropdownMenuActionViewItem { } } -class DropdownWithDefaultActionViewItem extends BaseActionViewItem { +export interface IDropdownWithDefaultActionViewItemOptions extends IDropdownMenuActionViewItemOptions { + renderKeybindingWithDefaultActionLabel?: boolean; +} + +export class DropdownWithDefaultActionViewItem extends BaseActionViewItem { + private readonly _options: IDropdownWithDefaultActionViewItemOptions | undefined; private _defaultAction: ActionViewItem; private _dropdown: DropdownMenuActionViewItem; private _container: HTMLElement | null = null; @@ -324,7 +331,7 @@ class DropdownWithDefaultActionViewItem extends BaseActionViewItem { constructor( submenuAction: SubmenuItemAction, - options: IDropdownMenuActionViewItemOptions | undefined, + options: IDropdownWithDefaultActionViewItemOptions | undefined, @IKeybindingService protected readonly _keybindingService: IKeybindingService, @INotificationService protected _notificationService: INotificationService, @IContextMenuService protected _contextMenuService: IContextMenuService, @@ -333,7 +340,7 @@ class DropdownWithDefaultActionViewItem extends BaseActionViewItem { @IStorageService protected _storageService: IStorageService ) { super(null, submenuAction); - + this._options = options; this._storageKey = `${submenuAction.item.submenu._debugName}_lastActionId`; // determine default action @@ -346,7 +353,7 @@ class DropdownWithDefaultActionViewItem extends BaseActionViewItem { defaultAction = submenuAction.actions[0]; } - this._defaultAction = this._instaService.createInstance(MenuEntryActionViewItem, <MenuItemAction>defaultAction, undefined); + this._defaultAction = this._instaService.createInstance(MenuEntryActionViewItem, <MenuItemAction>defaultAction, { keybinding: this._getDefaultActionKeybindingLabel(defaultAction) }); const dropdownOptions = Object.assign({}, options ?? Object.create(null), { menuAsChild: options?.menuAsChild ?? true, @@ -366,7 +373,7 @@ class DropdownWithDefaultActionViewItem extends BaseActionViewItem { this._storageService.store(this._storageKey, lastAction.id, StorageScope.WORKSPACE, StorageTarget.USER); this._defaultAction.dispose(); - this._defaultAction = this._instaService.createInstance(MenuEntryActionViewItem, lastAction, undefined); + this._defaultAction = this._instaService.createInstance(MenuEntryActionViewItem, lastAction, { keybinding: this._getDefaultActionKeybindingLabel(lastAction) }); this._defaultAction.actionRunner = new class extends ActionRunner { override async runAction(action: IAction, context?: unknown): Promise<void> { await action.run(undefined); @@ -378,6 +385,17 @@ class DropdownWithDefaultActionViewItem extends BaseActionViewItem { } } + private _getDefaultActionKeybindingLabel(defaultAction: IAction) { + let defaultActionKeybinding: string | undefined; + if (this._options?.renderKeybindingWithDefaultActionLabel) { + const kb = this._keybindingService.lookupKeybinding(defaultAction.id); + if (kb) { + defaultActionKeybinding = `(${kb.getLabel()})`; + } + } + return defaultActionKeybinding; + } + override setActionContext(newContext: unknown): void { super.setActionContext(newContext); this._defaultAction.setActionContext(newContext); diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 5a4f3089d64..e97c241b95c 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -54,6 +54,7 @@ export class MenuId { static readonly DebugVariablesContext = new MenuId('DebugVariablesContext'); static readonly DebugWatchContext = new MenuId('DebugWatchContext'); static readonly DebugToolBar = new MenuId('DebugToolBar'); + static readonly DebugToolBarStop = new MenuId('DebugToolBarStop'); static readonly EditorContext = new MenuId('EditorContext'); static readonly SimpleEditorContext = new MenuId('SimpleEditorContext'); static readonly EditorContextCopy = new MenuId('EditorContextCopy'); @@ -66,6 +67,8 @@ export class MenuId { static readonly ExplorerContext = new MenuId('ExplorerContext'); static readonly ExtensionContext = new MenuId('ExtensionContext'); static readonly GlobalActivity = new MenuId('GlobalActivity'); + static readonly TitleMenu = new MenuId('TitleMenu'); + static readonly TitleMenuQuickPick = new MenuId('TitleMenuQuickPick'); static readonly LayoutControlMenuSubmenu = new MenuId('LayoutControlMenuSubmenu'); static readonly LayoutControlMenu = new MenuId('LayoutControlMenu'); static readonly MenubarMainMenu = new MenuId('MenubarMainMenu'); diff --git a/src/vs/platform/credentials/common/credentialsMainService.ts b/src/vs/platform/credentials/common/credentialsMainService.ts index 6bed808b144..12a591de763 100644 --- a/src/vs/platform/credentials/common/credentialsMainService.ts +++ b/src/vs/platform/credentials/common/credentialsMainService.ts @@ -37,11 +37,22 @@ export abstract class BaseCredentialsMainService extends Disposable implements I public abstract getSecretStoragePrefix(): Promise<string>; protected abstract withKeytar(): Promise<KeytarModule>; + /** + * An optional method that subclasses can implement to assist in surfacing + * Keytar load errors to the user in a friendly way. + */ + protected abstract surfaceKeytarLoadError?: (err: any) => void; //#endregion async getPassword(service: string, account: string): Promise<string | null> { - const keytar = await this.withKeytar(); + let keytar: KeytarModule; + try { + keytar = await this.withKeytar(); + } catch (e) { + // for get operations, we don't want to surface errors to the user + return null; + } const password = await keytar.getPassword(service, account); if (password) { @@ -70,7 +81,14 @@ export abstract class BaseCredentialsMainService extends Disposable implements I } async setPassword(service: string, account: string, password: string): Promise<void> { - const keytar = await this.withKeytar(); + let keytar: KeytarModule; + try { + keytar = await this.withKeytar(); + } catch (e) { + this.surfaceKeytarLoadError?.(e); + throw e; + } + const MAX_SET_ATTEMPTS = 3; // Sometimes Keytar has a problem talking to the keychain on the OS. To be more resilient, we retry a few times. @@ -83,7 +101,7 @@ export abstract class BaseCredentialsMainService extends Disposable implements I return; } catch (e) { error = e; - this.logService.warn('Error attempting to set a password: ', e); + this.logService.warn('Error attempting to set a password: ', e?.message ?? e); attempts++; await new Promise(resolve => setTimeout(resolve, 200)); } @@ -119,9 +137,34 @@ export abstract class BaseCredentialsMainService extends Disposable implements I } async deletePassword(service: string, account: string): Promise<boolean> { - const keytar = await this.withKeytar(); + let keytar: KeytarModule; + try { + keytar = await this.withKeytar(); + } catch (e) { + this.surfaceKeytarLoadError?.(e); + throw e; + } + const password = await keytar.getPassword(service, account); + if (!password) { + return false; + } const didDelete = await keytar.deletePassword(service, account); + let { content, hasNextChunk }: ChunkedPassword = JSON.parse(password); + if (content && hasNextChunk) { + // need to delete additional chunks + let index = 1; + while (hasNextChunk) { + const accountWithIndex = `${account}-${index}`; + const nextChunk = await keytar.getPassword(service, accountWithIndex); + await keytar.deletePassword(service, accountWithIndex); + + const result: ChunkedPassword = JSON.parse(nextChunk!); + hasNextChunk = result.hasNextChunk; + index++; + } + } + if (didDelete) { this._onDidChangePassword.fire({ service, account }); } @@ -130,13 +173,25 @@ export abstract class BaseCredentialsMainService extends Disposable implements I } async findPassword(service: string): Promise<string | null> { - const keytar = await this.withKeytar(); + let keytar: KeytarModule; + try { + keytar = await this.withKeytar(); + } catch (e) { + // for get operations, we don't want to surface errors to the user + return null; + } return keytar.findPassword(service); } async findCredentials(service: string): Promise<Array<{ account: string; password: string }>> { - const keytar = await this.withKeytar(); + let keytar: KeytarModule; + try { + keytar = await this.withKeytar(); + } catch (e) { + // for get operations, we don't want to surface errors to the user + return []; + } return keytar.findCredentials(service); } diff --git a/src/vs/platform/credentials/electron-main/credentialsMainService.ts b/src/vs/platform/credentials/electron-main/credentialsMainService.ts index 0bf232d2153..f62719408c1 100644 --- a/src/vs/platform/credentials/electron-main/credentialsMainService.ts +++ b/src/vs/platform/credentials/electron-main/credentialsMainService.ts @@ -36,14 +36,14 @@ export class CredentialsNativeMainService extends BaseCredentialsMainService { return this._keytarCache; } - try { - this._keytarCache = await import('keytar'); - // Try using keytar to see if it throws or not. - await this._keytarCache.findCredentials('test-keytar-loads'); - } catch (e) { - this.windowsMainService.sendToFocused('vscode:showCredentialsError', e.message ?? e); - throw e; - } + const keytarCache = await import('keytar'); + // Try using keytar to see if it throws or not. + await keytarCache.findCredentials('test-keytar-loads'); + this._keytarCache = keytarCache; return this._keytarCache; } + + protected override surfaceKeytarLoadError = (err: any) => { + this.windowsMainService.sendToFocused('vscode:showCredentialsError', err.message ?? err); + }; } diff --git a/src/vs/platform/credentials/node/credentialsMainService.ts b/src/vs/platform/credentials/node/credentialsMainService.ts index f00f3532d00..cc2156d22cd 100644 --- a/src/vs/platform/credentials/node/credentialsMainService.ts +++ b/src/vs/platform/credentials/node/credentialsMainService.ts @@ -10,6 +10,9 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { BaseCredentialsMainService, KeytarModule } from 'vs/platform/credentials/common/credentialsMainService'; export class CredentialsWebMainService extends BaseCredentialsMainService { + // Since we fallback to the in-memory credentials provider, we do not need to surface any Keytar load errors + // to the user. + protected surfaceKeytarLoadError?: (err: any) => void; constructor( @ILogService logService: ILogService, diff --git a/src/vs/platform/diagnostics/electron-main/diagnosticsMainService.ts b/src/vs/platform/diagnostics/electron-main/diagnosticsMainService.ts index ca6c0f9cdd2..e8b236510d8 100644 --- a/src/vs/platform/diagnostics/electron-main/diagnosticsMainService.ts +++ b/src/vs/platform/diagnostics/electron-main/diagnosticsMainService.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event as IpcEvent, ipcMain } from 'electron'; +import { Event as IpcEvent } from 'electron'; +import { validatedIpcMain } from 'vs/base/parts/ipc/electron-main/ipcMain'; import { CancellationToken } from 'vs/base/common/cancellation'; import { URI } from 'vs/base/common/uri'; import { IDiagnosticInfo, IDiagnosticInfoOptions, IRemoteDiagnosticError, IRemoteDiagnosticInfo } from 'vs/platform/diagnostics/common/diagnostics'; @@ -49,7 +50,7 @@ export class DiagnosticsMainService implements IDiagnosticsMainService { window.sendWhenReady('vscode:getDiagnosticInfo', CancellationToken.None, { replyChannel, args }); - ipcMain.once(replyChannel, (_: IpcEvent, data: IRemoteDiagnosticInfo) => { + validatedIpcMain.once(replyChannel, (_: IpcEvent, data: IRemoteDiagnosticInfo) => { // No data is returned if getting the connection fails. if (!data) { resolve({ hostName: remoteAuthority, errorMessage: `Unable to resolve connection to '${remoteAuthority}'.` }); diff --git a/src/vs/platform/dialogs/common/dialogs.ts b/src/vs/platform/dialogs/common/dialogs.ts index 2ae876cdcc8..ace003cd2b4 100644 --- a/src/vs/platform/dialogs/common/dialogs.ts +++ b/src/vs/platform/dialogs/common/dialogs.ts @@ -14,49 +14,49 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { ITelemetryData } from 'vs/platform/telemetry/common/telemetry'; export interface FileFilter { - extensions: string[]; - name: string; + readonly extensions: string[]; + readonly name: string; } export type DialogType = 'none' | 'info' | 'error' | 'question' | 'warning'; export interface ICheckbox { - label: string; - checked?: boolean; + readonly label: string; + readonly checked?: boolean; } export interface IConfirmDialogArgs { - confirmation: IConfirmation; + readonly confirmation: IConfirmation; } export interface IShowDialogArgs { - severity: Severity; - message: string; - buttons?: string[]; - options?: IDialogOptions; + readonly severity: Severity; + readonly message: string; + readonly buttons?: string[]; + readonly options?: IDialogOptions; } export interface IInputDialogArgs extends IShowDialogArgs { - buttons: string[]; - inputs: IInput[]; + readonly buttons: string[]; + readonly inputs: IInput[]; } export interface IDialog { - confirmArgs?: IConfirmDialogArgs; - showArgs?: IShowDialogArgs; - inputArgs?: IInputDialogArgs; + readonly confirmArgs?: IConfirmDialogArgs; + readonly showArgs?: IShowDialogArgs; + readonly inputArgs?: IInputDialogArgs; } export type IDialogResult = IConfirmationResult | IInputResult | IShowResult; export interface IConfirmation { - title?: string; - type?: DialogType; - message: string; - detail?: string; - primaryButton?: string; - secondaryButton?: string; - checkbox?: ICheckbox; + readonly title?: string; + readonly type?: DialogType; + readonly message: string; + readonly detail?: string; + readonly primaryButton?: string; + readonly secondaryButton?: string; + readonly checkbox?: ICheckbox; } export interface IConfirmationResult { @@ -65,13 +65,13 @@ export interface IConfirmationResult { * Will be true if the dialog was confirmed with the primary button * pressed. */ - confirmed: boolean; + readonly confirmed: boolean; /** * This will only be defined if the confirmation was created * with the checkbox option defined. */ - checkboxChecked?: boolean; + readonly checkboxChecked?: boolean; } export interface IShowResult { @@ -81,13 +81,13 @@ export interface IShowResult { * then a promise with index of `cancelId` option is returned. If there is no such * option then promise with index `0` is returned. */ - choice: number; + readonly choice: number; /** * This will only be defined if the confirmation was created * with the checkbox option defined. */ - checkboxChecked?: boolean; + readonly checkboxChecked?: boolean; } export interface IInputResult extends IShowResult { @@ -96,7 +96,7 @@ export interface IInputResult extends IShowResult { * Values for the input fields as provided by the user * or `undefined` if none. */ - values?: string[]; + readonly values?: string[]; } export interface IPickAndOpenOptions { @@ -185,29 +185,29 @@ export interface IOpenDialogOptions { export const IDialogService = createDecorator<IDialogService>('dialogService'); export interface ICustomDialogOptions { - buttonDetails?: string[]; - markdownDetails?: ICustomDialogMarkdown[]; - classes?: string[]; - icon?: Codicon; - disableCloseAction?: boolean; + readonly buttonDetails?: string[]; + readonly markdownDetails?: ICustomDialogMarkdown[]; + readonly classes?: string[]; + readonly icon?: Codicon; + readonly disableCloseAction?: boolean; } export interface ICustomDialogMarkdown { - markdown: IMarkdownString; - classes?: string[]; + readonly markdown: IMarkdownString; + readonly classes?: string[]; } export interface IDialogOptions { - cancelId?: number; - detail?: string; - checkbox?: ICheckbox; - custom?: boolean | ICustomDialogOptions; + readonly cancelId?: number; + readonly detail?: string; + readonly checkbox?: ICheckbox; + readonly custom?: boolean | ICustomDialogOptions; } export interface IInput { - placeholder?: string; - type?: 'text' | 'password'; - value?: string; + readonly placeholder?: string; + readonly type?: 'text' | 'password'; + readonly value?: string; } /** @@ -323,7 +323,7 @@ export interface IFileDialogService { * @param schemeFilter The scheme of the workspace path. If no filter given, the scheme of the current window is used. * Falls back to user home in the absence of enough information to find a better URI. */ - defaultWorkspacePath(schemeFilter?: string, filename?: string): Promise<URI>; + defaultWorkspacePath(schemeFilter?: string): Promise<URI>; /** * Shows a file-folder selection dialog and opens the selected entry. @@ -390,10 +390,10 @@ export function getFileNamesMessage(fileNamesOrResources: readonly (string | URI } export interface INativeOpenDialogOptions { - forceNewWindow?: boolean; + readonly forceNewWindow?: boolean; - defaultPath?: string; + readonly defaultPath?: string; - telemetryEventName?: string; - telemetryExtraData?: ITelemetryData; + readonly telemetryEventName?: string; + readonly telemetryExtraData?: ITelemetryData; } diff --git a/src/vs/platform/dialogs/electron-main/dialogMainService.ts b/src/vs/platform/dialogs/electron-main/dialogMainService.ts index 975036b0a5a..6bc6ec776e0 100644 --- a/src/vs/platform/dialogs/electron-main/dialogMainService.ts +++ b/src/vs/platform/dialogs/electron-main/dialogMainService.ts @@ -9,7 +9,6 @@ import { hash } from 'vs/base/common/hash'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { Disposable, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { normalizeNFC } from 'vs/base/common/normalization'; -import { dirname } from 'vs/base/common/path'; import { isMacintosh } from 'vs/base/common/platform'; import { withNullAsUndefined } from 'vs/base/common/types'; import { Promises } from 'vs/base/node/pfs'; @@ -17,7 +16,6 @@ import { localize } from 'vs/nls'; import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; -import { IStateMainService } from 'vs/platform/state/electron-main/state'; import { WORKSPACE_FILTER } from 'vs/platform/workspace/common/workspace'; export const IDialogMainService = createDecorator<IDialogMainService>('dialogMainService'); @@ -37,26 +35,23 @@ export interface IDialogMainService { } interface IInternalNativeOpenDialogOptions extends INativeOpenDialogOptions { - pickFolders?: boolean; - pickFiles?: boolean; + readonly pickFolders?: boolean; + readonly pickFiles?: boolean; - title: string; - buttonLabel?: string; - filters?: FileFilter[]; + readonly title: string; + readonly buttonLabel?: string; + readonly filters?: FileFilter[]; } export class DialogMainService implements IDialogMainService { declare readonly _serviceBrand: undefined; - private static readonly workingDirPickerStorageKey = 'pickerWorkingDir'; - private readonly windowFileDialogLocks = new Map<number, Set<number>>(); private readonly windowDialogQueues = new Map<number, Queue<MessageBoxReturnValue | SaveDialogReturnValue | OpenDialogReturnValue>>(); private readonly noWindowDialogueQueue = new Queue<MessageBoxReturnValue | SaveDialogReturnValue | OpenDialogReturnValue>(); constructor( - @IStateMainService private readonly stateMainService: IStateMainService, @ILogService private readonly logService: ILogService ) { } @@ -90,9 +85,6 @@ export class DialogMainService implements IDialogMainService { filters: options.filters }; - // Ensure defaultPath - dialogOptions.defaultPath = options.defaultPath || this.stateMainService.getItem<string>(DialogMainService.workingDirPickerStorageKey); - // Ensure properties if (typeof options.pickFiles === 'boolean' || typeof options.pickFolders === 'boolean') { dialogOptions.properties = undefined; // let it override based on the booleans @@ -111,18 +103,12 @@ export class DialogMainService implements IDialogMainService { } // Show Dialog - const windowToUse = window || BrowserWindow.getFocusedWindow(); - - const result = await this.showOpenDialog(dialogOptions, withNullAsUndefined(windowToUse)); + const result = await this.showOpenDialog(dialogOptions, withNullAsUndefined(window || BrowserWindow.getFocusedWindow())); if (result && result.filePaths && result.filePaths.length > 0) { - - // Remember path in storage for next time - this.stateMainService.setItem(DialogMainService.workingDirPickerStorageKey, dirname(result.filePaths[0])); - return result.filePaths; } - return; + return undefined; } private getWindowDialogQueue<T extends MessageBoxReturnValue | SaveDialogReturnValue | OpenDialogReturnValue>(window?: BrowserWindow): Queue<T> { @@ -154,7 +140,7 @@ export class DialogMainService implements IDialogMainService { async showSaveDialog(options: SaveDialogOptions, window?: BrowserWindow): Promise<SaveDialogReturnValue> { - // prevent duplicates of the same dialog queueing at the same time + // Prevent duplicates of the same dialog queueing at the same time const fileDialogLock = this.acquireFileDialogLock(options, window); if (!fileDialogLock) { this.logService.error('[DialogMainService]: file save dialog is already or will be showing for the window with the same configuration'); @@ -204,7 +190,7 @@ export class DialogMainService implements IDialogMainService { } } - // prevent duplicates of the same dialog queueing at the same time + // Prevent duplicates of the same dialog queueing at the same time const fileDialogLock = this.acquireFileDialogLock(options, window); if (!fileDialogLock) { this.logService.error('[DialogMainService]: file open dialog is already or will be showing for the window with the same configuration'); @@ -232,13 +218,13 @@ export class DialogMainService implements IDialogMainService { private acquireFileDialogLock(options: SaveDialogOptions | OpenDialogOptions, window?: BrowserWindow): IDisposable | undefined { - // if no window is provided, allow as many dialogs as + // If no window is provided, allow as many dialogs as // needed since we consider them not modal per window if (!window) { return Disposable.None; } - // if a window is provided, only allow a single dialog + // If a window is provided, only allow a single dialog // at the same time because dialogs are modal and we // do not want to open one dialog after the other // (https://github.com/microsoft/vscode/issues/114432) @@ -267,7 +253,7 @@ export class DialogMainService implements IDialogMainService { windowFileDialogLocks?.delete(optionsHash); - // if the window has no more dialog locks, delete it from the set of locks + // If the window has no more dialog locks, delete it from the set of locks if (windowFileDialogLocks?.size === 0) { this.windowFileDialogLocks.delete(window.id); } diff --git a/src/vs/platform/download/common/downloadService.ts b/src/vs/platform/download/common/downloadService.ts index 78d52e18fb5..3541b4abf13 100644 --- a/src/vs/platform/download/common/downloadService.ts +++ b/src/vs/platform/download/common/downloadService.ts @@ -8,7 +8,7 @@ import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { IDownloadService } from 'vs/platform/download/common/download'; import { IFileService } from 'vs/platform/files/common/files'; -import { asText, IRequestService } from 'vs/platform/request/common/request'; +import { asTextOrError, IRequestService } from 'vs/platform/request/common/request'; export class DownloadService implements IDownloadService { @@ -30,7 +30,7 @@ export class DownloadService implements IDownloadService { if (context.res.statusCode === 200) { await this.fileService.writeFile(target, context.stream); } else { - const message = await asText(context); + const message = await asTextOrError(context); throw new Error(`Expected 200, got back ${context.res.statusCode} instead.\n\n${message}`); } } diff --git a/src/vs/platform/driver/browser/driver.ts b/src/vs/platform/driver/browser/driver.ts index c57855c3828..c16032c953f 100644 --- a/src/vs/platform/driver/browser/driver.ts +++ b/src/vs/platform/driver/browser/driver.ts @@ -205,6 +205,10 @@ export class BrowserWindowDriver implements IWindowDriver { throw new Error('Method not implemented.'); } + + async exitApplication(): Promise<void> { + // No-op in web + } } export function registerWindowDriver(): void { diff --git a/src/vs/platform/driver/common/driver.ts b/src/vs/platform/driver/common/driver.ts index 6d23ce9748c..1c593be20d1 100644 --- a/src/vs/platform/driver/common/driver.ts +++ b/src/vs/platform/driver/common/driver.ts @@ -3,8 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; - // !! Do not remove the following START and END markers, they are parsed by the smoketest build //*START @@ -19,14 +17,7 @@ export interface IElement { } export interface ILocaleInfo { - /** - * The UI language used. - */ language: string; - - /** - * The requested locale - */ locale?: string; } @@ -36,29 +27,6 @@ export interface ILocalizedStrings { find: string; } -export interface IDriver { - readonly _serviceBrand: undefined; - - getWindowIds(): Promise<number[]>; - capturePage(windowId: number): Promise<string>; - startTracing(windowId: number, name: string): Promise<void>; - stopTracing(windowId: number, name: string, persist: boolean): Promise<void>; - reloadWindow(windowId: number): Promise<void>; - exitApplication(): Promise<number /* main PID */>; - dispatchKeybinding(windowId: number, keybinding: string): Promise<void>; - click(windowId: number, selector: string, xoffset?: number | undefined, yoffset?: number | undefined): Promise<void>; - setValue(windowId: number, selector: string, text: string): Promise<void>; - getTitle(windowId: number): Promise<string>; - isActiveElement(windowId: number, selector: string): Promise<boolean>; - getElements(windowId: number, selector: string, recursive?: boolean): Promise<IElement[]>; - getElementXY(windowId: number, selector: string, xoffset?: number, yoffset?: number): Promise<{ x: number; y: number }>; - typeInEditor(windowId: number, selector: string, text: string): Promise<void>; - getTerminalBuffer(windowId: number, selector: string): Promise<string[]>; - writeInTerminal(windowId: number, selector: string, text: string): Promise<void>; - getLocaleInfo(windowId: number): Promise<ILocaleInfo>; - getLocalizedStrings(windowId: number): Promise<ILocalizedStrings>; -} - export interface IWindowDriver { click(selector: string, xoffset?: number | undefined, yoffset?: number | undefined): Promise<void>; setValue(selector: string, text: string): Promise<void>; @@ -71,13 +39,6 @@ export interface IWindowDriver { writeInTerminal(selector: string, text: string): Promise<void>; getLocaleInfo(): Promise<ILocaleInfo>; getLocalizedStrings(): Promise<ILocalizedStrings>; + exitApplication(): Promise<void>; } //*END - -export const ID = 'driverService'; -export const IDriver = createDecorator<IDriver>(ID); - -export interface IWindowDriverRegistry { - registerWindowDriver(windowId: number): Promise<void>; - reloadWindowDriver(windowId: number): Promise<void>; -} diff --git a/src/vs/platform/driver/common/driverIpc.ts b/src/vs/platform/driver/common/driverIpc.ts deleted file mode 100644 index 6d5ed5e55c4..00000000000 --- a/src/vs/platform/driver/common/driverIpc.ts +++ /dev/null @@ -1,101 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Event } from 'vs/base/common/event'; -import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IElement, ILocaleInfo, ILocalizedStrings as ILocalizedStrings, IWindowDriver, IWindowDriverRegistry } from 'vs/platform/driver/common/driver'; - -export class WindowDriverChannel implements IServerChannel { - - constructor(private driver: IWindowDriver) { } - - listen<T>(_: unknown, event: string): Event<T> { - throw new Error(`No event found: ${event}`); - } - - call(_: unknown, command: string, arg?: any): Promise<any> { - switch (command) { - case 'click': return this.driver.click(arg[0], arg[1], arg[2]); - case 'setValue': return this.driver.setValue(arg[0], arg[1]); - case 'getTitle': return this.driver.getTitle(); - case 'isActiveElement': return this.driver.isActiveElement(arg); - case 'getElements': return this.driver.getElements(arg[0], arg[1]); - case 'getElementXY': return this.driver.getElementXY(arg[0], arg[1], arg[2]); - case 'typeInEditor': return this.driver.typeInEditor(arg[0], arg[1]); - case 'getTerminalBuffer': return this.driver.getTerminalBuffer(arg); - case 'writeInTerminal': return this.driver.writeInTerminal(arg[0], arg[1]); - case 'getLocaleInfo': return this.driver.getLocaleInfo(); - case 'getLocalizedStrings': return this.driver.getLocalizedStrings(); - } - - throw new Error(`Call not found: ${command}`); - } -} - -export class WindowDriverChannelClient implements IWindowDriver { - - declare readonly _serviceBrand: undefined; - - constructor(private channel: IChannel) { } - - click(selector: string, xoffset?: number, yoffset?: number): Promise<void> { - return this.channel.call('click', [selector, xoffset, yoffset]); - } - - setValue(selector: string, text: string): Promise<void> { - return this.channel.call('setValue', [selector, text]); - } - - getTitle(): Promise<string> { - return this.channel.call('getTitle'); - } - - isActiveElement(selector: string): Promise<boolean> { - return this.channel.call('isActiveElement', selector); - } - - getElements(selector: string, recursive: boolean): Promise<IElement[]> { - return this.channel.call('getElements', [selector, recursive]); - } - - getElementXY(selector: string, xoffset?: number, yoffset?: number): Promise<{ x: number; y: number }> { - return this.channel.call('getElementXY', [selector, xoffset, yoffset]); - } - - typeInEditor(selector: string, text: string): Promise<void> { - return this.channel.call('typeInEditor', [selector, text]); - } - - getTerminalBuffer(selector: string): Promise<string[]> { - return this.channel.call('getTerminalBuffer', selector); - } - - writeInTerminal(selector: string, text: string): Promise<void> { - return this.channel.call('writeInTerminal', [selector, text]); - } - - getLocaleInfo(): Promise<ILocaleInfo> { - return this.channel.call('getLocaleInfo'); - } - - getLocalizedStrings(): Promise<ILocalizedStrings> { - return this.channel.call('getLocalizedStrings'); - } -} - -export class WindowDriverRegistryChannelClient implements IWindowDriverRegistry { - - declare readonly _serviceBrand: undefined; - - constructor(private channel: IChannel) { } - - registerWindowDriver(windowId: number): Promise<void> { - return this.channel.call('registerWindowDriver', windowId); - } - - reloadWindowDriver(windowId: number): Promise<void> { - return this.channel.call('reloadWindowDriver', windowId); - } -} diff --git a/src/vs/platform/driver/electron-main/driver.ts b/src/vs/platform/driver/electron-main/driver.ts deleted file mode 100644 index f14f14e40ad..00000000000 --- a/src/vs/platform/driver/electron-main/driver.ts +++ /dev/null @@ -1,259 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { timeout } from 'vs/base/common/async'; -import { Emitter, Event } from 'vs/base/common/event'; -import { KeybindingParser } from 'vs/base/common/keybindingParser'; -import { KeyCode } from 'vs/base/common/keyCodes'; -import { SimpleKeybinding, ScanCodeBinding } from 'vs/base/common/keybindings'; -import { combinedDisposable, IDisposable } from 'vs/base/common/lifecycle'; -import { OS } from 'vs/base/common/platform'; -import { IPCServer, StaticRouter } from 'vs/base/parts/ipc/common/ipc'; -import { serve as serveNet } from 'vs/base/parts/ipc/node/ipc.net'; -import { IDriver, IElement, ILocaleInfo, ILocalizedStrings, IWindowDriver, IWindowDriverRegistry } from 'vs/platform/driver/common/driver'; -import { WindowDriverChannelClient } from 'vs/platform/driver/common/driverIpc'; -import { DriverChannel, WindowDriverRegistryChannel } from 'vs/platform/driver/node/driver'; -import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding'; -import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; -import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; -import { IFileService } from 'vs/platform/files/common/files'; -import { URI } from 'vs/base/common/uri'; -import { join } from 'vs/base/common/path'; -import { VSBuffer } from 'vs/base/common/buffer'; -import { ILogService } from 'vs/platform/log/common/log'; - -function isSilentKeyCode(keyCode: KeyCode) { - return keyCode < KeyCode.Digit0; -} - -export class Driver implements IDriver, IWindowDriverRegistry { - - declare readonly _serviceBrand: undefined; - - private registeredWindowIds = new Set<number>(); - private reloadingWindowIds = new Set<number>(); - private readonly onDidReloadingChange = new Emitter<void>(); - - constructor( - private windowServer: IPCServer, - @IWindowsMainService private readonly windowsMainService: IWindowsMainService, - @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, - @IFileService private readonly fileService: IFileService, - @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService, - @ILogService private readonly logService: ILogService - ) { } - - async registerWindowDriver(windowId: number): Promise<void> { - this.logService.info(`[driver] registerWindowDriver(${windowId})`); - - this.registeredWindowIds.add(windowId); - this.reloadingWindowIds.delete(windowId); - this.onDidReloadingChange.fire(); - } - - async reloadWindowDriver(windowId: number): Promise<void> { - this.logService.info(`[driver] reloadWindowDriver(${windowId})`); - - this.reloadingWindowIds.add(windowId); - } - - async getWindowIds(): Promise<number[]> { - const windowIds = this.windowsMainService.getWindows() - .map(window => window.id) - .filter(windowId => this.registeredWindowIds.has(windowId) && !this.reloadingWindowIds.has(windowId)); - - return windowIds; - } - - async capturePage(windowId: number): Promise<string> { - const window = this.windowsMainService.getWindowById(windowId) ?? this.windowsMainService.getLastActiveWindow(); // fallback to active window to ensure we capture window - if (!window?.win) { - throw new Error('Invalid window'); - } - - const webContents = window.win.webContents; - const image = await webContents.capturePage(); - return image.toPNG().toString('base64'); - } - - async startTracing(windowId: number, name: string): Promise<void> { - // ignore - tracing is not implemented yet - } - - async stopTracing(windowId: number, name: string, persist: boolean): Promise<void> { - if (!persist) { - return; - } - - const raw = await this.capturePage(windowId); - const buffer = Buffer.from(raw, 'base64'); - - await this.fileService.writeFile(URI.file(join(this.environmentMainService.logsPath, `${name}.png`)), VSBuffer.wrap(buffer)); - } - - async reloadWindow(windowId: number): Promise<void> { - this.logService.info(`[driver] reloadWindow(${windowId})`); - - await this.whenUnfrozen(windowId); - - const window = this.windowsMainService.getWindowById(windowId); - if (!window) { - throw new Error('Invalid window'); - } - - this.reloadingWindowIds.add(windowId); - this.lifecycleMainService.reload(window); - } - - async exitApplication(): Promise<number> { - this.logService.info(`[driver] exitApplication()`); - - this.lifecycleMainService.quit(); - - return process.pid; - } - - async dispatchKeybinding(windowId: number, keybinding: string): Promise<void> { - await this.whenUnfrozen(windowId); - - const parts = KeybindingParser.parseUserBinding(keybinding); - - for (let part of parts) { - await this._dispatchKeybinding(windowId, part); - } - } - - private async _dispatchKeybinding(windowId: number, keybinding: SimpleKeybinding | ScanCodeBinding): Promise<void> { - if (keybinding instanceof ScanCodeBinding) { - throw new Error('ScanCodeBindings not supported'); - } - - const window = this.windowsMainService.getWindowById(windowId); - if (!window?.win) { - throw new Error('Invalid window'); - } - const webContents = window.win.webContents; - const noModifiedKeybinding = new SimpleKeybinding(false, false, false, false, keybinding.keyCode); - const resolvedKeybinding = new USLayoutResolvedKeybinding(noModifiedKeybinding.toChord(), OS); - const keyCode = resolvedKeybinding.getElectronAccelerator(); - - const modifiers: string[] = []; - - if (keybinding.ctrlKey) { - modifiers.push('ctrl'); - } - - if (keybinding.metaKey) { - modifiers.push('meta'); - } - - if (keybinding.shiftKey) { - modifiers.push('shift'); - } - - if (keybinding.altKey) { - modifiers.push('alt'); - } - - webContents.sendInputEvent({ type: 'keyDown', keyCode, modifiers } as any); - - if (!isSilentKeyCode(keybinding.keyCode)) { - webContents.sendInputEvent({ type: 'char', keyCode, modifiers } as any); - } - - webContents.sendInputEvent({ type: 'keyUp', keyCode, modifiers } as any); - - await timeout(100); - } - - async click(windowId: number, selector: string, xoffset?: number, yoffset?: number): Promise<void> { - const windowDriver = await this.getWindowDriver(windowId); - await windowDriver.click(selector, xoffset, yoffset); - } - - async setValue(windowId: number, selector: string, text: string): Promise<void> { - const windowDriver = await this.getWindowDriver(windowId); - await windowDriver.setValue(selector, text); - } - - async getTitle(windowId: number): Promise<string> { - const windowDriver = await this.getWindowDriver(windowId); - return await windowDriver.getTitle(); - } - - async isActiveElement(windowId: number, selector: string): Promise<boolean> { - const windowDriver = await this.getWindowDriver(windowId); - return await windowDriver.isActiveElement(selector); - } - - async getElements(windowId: number, selector: string, recursive: boolean): Promise<IElement[]> { - const windowDriver = await this.getWindowDriver(windowId); - return await windowDriver.getElements(selector, recursive); - } - - async getElementXY(windowId: number, selector: string, xoffset?: number, yoffset?: number): Promise<{ x: number; y: number }> { - const windowDriver = await this.getWindowDriver(windowId); - return await windowDriver.getElementXY(selector, xoffset, yoffset); - } - - async typeInEditor(windowId: number, selector: string, text: string): Promise<void> { - const windowDriver = await this.getWindowDriver(windowId); - await windowDriver.typeInEditor(selector, text); - } - - async getTerminalBuffer(windowId: number, selector: string): Promise<string[]> { - const windowDriver = await this.getWindowDriver(windowId); - return await windowDriver.getTerminalBuffer(selector); - } - - async writeInTerminal(windowId: number, selector: string, text: string): Promise<void> { - const windowDriver = await this.getWindowDriver(windowId); - await windowDriver.writeInTerminal(selector, text); - } - - async getLocaleInfo(windowId: number): Promise<ILocaleInfo> { - const windowDriver = await this.getWindowDriver(windowId); - return await windowDriver.getLocaleInfo(); - } - - async getLocalizedStrings(windowId: number): Promise<ILocalizedStrings> { - const windowDriver = await this.getWindowDriver(windowId); - return await windowDriver.getLocalizedStrings(); - } - - private async getWindowDriver(windowId: number): Promise<IWindowDriver> { - await this.whenUnfrozen(windowId); - - const id = `window:${windowId}`; - const router = new StaticRouter(ctx => ctx === id); - const windowDriverChannel = this.windowServer.getChannel('windowDriver', router); - return new WindowDriverChannelClient(windowDriverChannel); - } - - private async whenUnfrozen(windowId: number): Promise<void> { - while (this.reloadingWindowIds.has(windowId)) { - await Event.toPromise(this.onDidReloadingChange.event); - } - } -} - -export async function serve( - windowServer: IPCServer, - handle: string, - instantiationService: IInstantiationService -): Promise<IDisposable> { - const driver = instantiationService.createInstance(Driver, windowServer); - - const windowDriverRegistryChannel = new WindowDriverRegistryChannel(driver); - windowServer.registerChannel('windowDriverRegistry', windowDriverRegistryChannel); - - const server = await serveNet(handle); - const channel = new DriverChannel(driver); - server.registerChannel('driver', channel); - - return combinedDisposable(server, windowServer); -} diff --git a/src/vs/platform/driver/electron-sandbox/driver.ts b/src/vs/platform/driver/electron-sandbox/driver.ts index bc2e4c9ae38..fb9b9a596ff 100644 --- a/src/vs/platform/driver/electron-sandbox/driver.ts +++ b/src/vs/platform/driver/electron-sandbox/driver.ts @@ -3,16 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { timeout } from 'vs/base/common/async'; -import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { BrowserWindowDriver } from 'vs/platform/driver/browser/driver'; -import { WindowDriverChannel, WindowDriverRegistryChannelClient } from 'vs/platform/driver/common/driverIpc'; -import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/services'; -import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; interface INativeWindowDriverHelper { - exitApplication(): Promise<number /* main process PID */>; + exitApplication(): Promise<void>; } class NativeWindowDriver extends BrowserWindowDriver { @@ -21,7 +15,7 @@ class NativeWindowDriver extends BrowserWindowDriver { super(); } - exitApplication(): Promise<number> { + override exitApplication(): Promise<void> { return this.helper.exitApplication(); } } @@ -29,50 +23,3 @@ class NativeWindowDriver extends BrowserWindowDriver { export function registerWindowDriver(helper: INativeWindowDriverHelper): void { Object.assign(window, { driver: new NativeWindowDriver(helper) }); } - -class LegacyNativeWindowDriver extends BrowserWindowDriver { - - constructor( - @INativeHostService private readonly nativeHostService: INativeHostService - ) { - super(); - } - - override click(selector: string, xoffset?: number, yoffset?: number): Promise<void> { - const offset = typeof xoffset === 'number' && typeof yoffset === 'number' ? { x: xoffset, y: yoffset } : undefined; - - return this.doClick(selector, 1, offset); - } - - private async doClick(selector: string, clickCount: number, offset?: { x: number; y: number }): Promise<void> { - const { x, y } = await this._getElementXY(selector, offset); - - await this.nativeHostService.sendInputEvent({ type: 'mouseDown', x, y, button: 'left', clickCount } as any); - await timeout(10); - - await this.nativeHostService.sendInputEvent({ type: 'mouseUp', x, y, button: 'left', clickCount } as any); - await timeout(100); - } -} - -/** - * Old school window driver that is implemented by us - * from the main process. - * - * @deprecated - */ -export async function registerLegacyWindowDriver(accessor: ServicesAccessor, windowId: number): Promise<IDisposable> { - const instantiationService = accessor.get(IInstantiationService); - const mainProcessService = accessor.get(IMainProcessService); - - const windowDriver = instantiationService.createInstance(LegacyNativeWindowDriver); - const windowDriverChannel = new WindowDriverChannel(windowDriver); - mainProcessService.registerChannel('windowDriver', windowDriverChannel); - - const windowDriverRegistryChannel = mainProcessService.getChannel('windowDriverRegistry'); - const windowDriverRegistry = new WindowDriverRegistryChannelClient(windowDriverRegistryChannel); - - await windowDriverRegistry.registerWindowDriver(windowId); - - return toDisposable(() => windowDriverRegistry.reloadWindowDriver(windowId)); -} diff --git a/src/vs/platform/driver/node/driver.ts b/src/vs/platform/driver/node/driver.ts deleted file mode 100644 index 29abf734bec..00000000000 --- a/src/vs/platform/driver/node/driver.ts +++ /dev/null @@ -1,148 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Event } from 'vs/base/common/event'; -import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; -import { Client } from 'vs/base/parts/ipc/common/ipc.net'; -import { connect as connectNet } from 'vs/base/parts/ipc/node/ipc.net'; -import { IDriver, IElement, ILocaleInfo, ILocalizedStrings, IWindowDriverRegistry } from 'vs/platform/driver/common/driver'; - -export class DriverChannel implements IServerChannel { - - constructor(private driver: IDriver) { } - - listen<T>(_: unknown, event: string): Event<T> { - throw new Error('No event found'); - } - - call(_: unknown, command: string, arg?: any): Promise<any> { - switch (command) { - case 'getWindowIds': return this.driver.getWindowIds(); - case 'capturePage': return this.driver.capturePage(arg); - case 'startTracing': return this.driver.startTracing(arg[0], arg[1]); - case 'stopTracing': return this.driver.stopTracing(arg[0], arg[1], arg[2]); - case 'reloadWindow': return this.driver.reloadWindow(arg); - case 'exitApplication': return this.driver.exitApplication(); - case 'dispatchKeybinding': return this.driver.dispatchKeybinding(arg[0], arg[1]); - case 'click': return this.driver.click(arg[0], arg[1], arg[2], arg[3]); - case 'setValue': return this.driver.setValue(arg[0], arg[1], arg[2]); - case 'getTitle': return this.driver.getTitle(arg[0]); - case 'isActiveElement': return this.driver.isActiveElement(arg[0], arg[1]); - case 'getElements': return this.driver.getElements(arg[0], arg[1], arg[2]); - case 'getElementXY': return this.driver.getElementXY(arg[0], arg[1], arg[2]); - case 'typeInEditor': return this.driver.typeInEditor(arg[0], arg[1], arg[2]); - case 'getTerminalBuffer': return this.driver.getTerminalBuffer(arg[0], arg[1]); - case 'writeInTerminal': return this.driver.writeInTerminal(arg[0], arg[1], arg[2]); - case 'getLocaleInfo': return this.driver.getLocaleInfo(arg); - case 'getLocalizedStrings': return this.driver.getLocalizedStrings(arg); - } - - throw new Error(`Call not found: ${command}`); - } -} - -export class DriverChannelClient implements IDriver { - - declare readonly _serviceBrand: undefined; - - constructor(private channel: IChannel) { } - - getWindowIds(): Promise<number[]> { - return this.channel.call('getWindowIds'); - } - - capturePage(windowId: number): Promise<string> { - return this.channel.call('capturePage', windowId); - } - - startTracing(windowId: number, name: string): Promise<void> { - return this.channel.call('startTracing', [windowId, name]); - } - - stopTracing(windowId: number, name: string, persist: boolean): Promise<void> { - return this.channel.call('stopTracing', [windowId, name, persist]); - } - - reloadWindow(windowId: number): Promise<void> { - return this.channel.call('reloadWindow', windowId); - } - - exitApplication(): Promise<number> { - return this.channel.call('exitApplication'); - } - - dispatchKeybinding(windowId: number, keybinding: string): Promise<void> { - return this.channel.call('dispatchKeybinding', [windowId, keybinding]); - } - - click(windowId: number, selector: string, xoffset: number | undefined, yoffset: number | undefined): Promise<void> { - return this.channel.call('click', [windowId, selector, xoffset, yoffset]); - } - - setValue(windowId: number, selector: string, text: string): Promise<void> { - return this.channel.call('setValue', [windowId, selector, text]); - } - - getTitle(windowId: number): Promise<string> { - return this.channel.call('getTitle', [windowId]); - } - - isActiveElement(windowId: number, selector: string): Promise<boolean> { - return this.channel.call('isActiveElement', [windowId, selector]); - } - - getElements(windowId: number, selector: string, recursive: boolean): Promise<IElement[]> { - return this.channel.call('getElements', [windowId, selector, recursive]); - } - - getElementXY(windowId: number, selector: string, xoffset: number | undefined, yoffset: number | undefined): Promise<{ x: number; y: number }> { - return this.channel.call('getElementXY', [windowId, selector, xoffset, yoffset]); - } - - typeInEditor(windowId: number, selector: string, text: string): Promise<void> { - return this.channel.call('typeInEditor', [windowId, selector, text]); - } - - getTerminalBuffer(windowId: number, selector: string): Promise<string[]> { - return this.channel.call('getTerminalBuffer', [windowId, selector]); - } - - writeInTerminal(windowId: number, selector: string, text: string): Promise<void> { - return this.channel.call('writeInTerminal', [windowId, selector, text]); - } - - getLocaleInfo(windowId: number): Promise<ILocaleInfo> { - return this.channel.call('getLocaleInfo', windowId); - } - - getLocalizedStrings(windowId: number): Promise<ILocalizedStrings> { - return this.channel.call('getLocalizedStrings', windowId); - } -} - -export class WindowDriverRegistryChannel implements IServerChannel { - - constructor(private registry: IWindowDriverRegistry) { } - - listen<T>(_: unknown, event: string): Event<T> { - throw new Error(`Event not found: ${event}`); - } - - call(_: unknown, command: string, arg?: any): Promise<any> { - switch (command) { - case 'registerWindowDriver': return this.registry.registerWindowDriver(arg); - case 'reloadWindowDriver': return this.registry.reloadWindowDriver(arg); - } - - throw new Error(`Call not found: ${command}`); - } -} - -export async function connect(handle: string): Promise<{ client: Client; driver: IDriver }> { - const client = await connectNet(handle, 'driverClient'); - const channel = client.getChannel('driver'); - const driver = new DriverChannelClient(channel); - return { client, driver }; -} diff --git a/src/vs/platform/editor/common/editor.ts b/src/vs/platform/editor/common/editor.ts index 7403c0916ef..31b9f1bba71 100644 --- a/src/vs/platform/editor/common/editor.ts +++ b/src/vs/platform/editor/common/editor.ts @@ -265,6 +265,15 @@ export interface IEditorOptions { * Will not show an error in case opening the editor fails and thus allows to show a custom error * message as needed. By default, an error will be presented as notification if opening was not possible. */ + + /** + * In case of an error opening the editor, will not present this error to the user (e.g. by showing + * a generic placeholder in the editor area). So it is up to the caller to provide error information + * in that case. + * + * By default, an error when opening an editor will result in a placeholder editor that shows the error. + * In certain cases a modal dialog may be presented to ask the user for further action. + */ ignoreError?: boolean; /** diff --git a/src/vs/platform/environment/common/argv.ts b/src/vs/platform/environment/common/argv.ts index ccac10545b5..b2b4500a614 100644 --- a/src/vs/platform/environment/common/argv.ts +++ b/src/vs/platform/environment/common/argv.ts @@ -79,10 +79,6 @@ export interface NativeParsedArgs { 'max-memory'?: string; 'file-write'?: boolean; 'file-chmod'?: boolean; - /** - * @deprecated use `enable-smoke-test-driver` - */ - 'driver'?: string; 'enable-smoke-test-driver'?: boolean; 'remote'?: string; 'force'?: boolean; diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts index 28a690c69bb..56531a3a784 100644 --- a/src/vs/platform/environment/common/environment.ts +++ b/src/vs/platform/environment/common/environment.ts @@ -132,9 +132,6 @@ export interface INativeEnvironmentService extends IEnvironmentService { extensionsDownloadPath: string; builtinExtensionsPath: string; - // --- smoke test support - driverHandle?: string; - // --- use keytar for credentials disableKeytar?: boolean; diff --git a/src/vs/platform/environment/common/environmentService.ts b/src/vs/platform/environment/common/environmentService.ts index ac02eadcbdb..fcea3f58023 100644 --- a/src/vs/platform/environment/common/environmentService.ts +++ b/src/vs/platform/environment/common/environmentService.ts @@ -230,8 +230,6 @@ export abstract class AbstractNativeEnvironmentService implements INativeEnviron get crashReporterId(): string | undefined { return this.args['crash-reporter-id']; } get crashReporterDirectory(): string | undefined { return this.args['crash-reporter-directory']; } - get driverHandle(): string | undefined { return this.args['driver']; } - @memoize get telemetryLogResource(): URI { return URI.file(join(this.logsPath, 'telemetry.log')); } get disableTelemetry(): boolean { return !!this.args['disable-telemetry']; } diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 1d7aadc1f14..062ce48a580 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -98,7 +98,6 @@ export const OPTIONS: OptionDescriptions<Required<NativeParsedArgs>> = { 'inspect-brk-search': { type: 'string', deprecates: ['debugBrkSearch'] }, 'export-default-configuration': { type: 'string' }, 'install-source': { type: 'string' }, - 'driver': { type: 'string' }, 'enable-smoke-test-driver': { type: 'boolean' }, 'logExtensionHostCommunication': { type: 'boolean' }, 'skip-release-notes': { type: 'boolean' }, diff --git a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts index 574a72fa436..81fa42981c2 100644 --- a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts +++ b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts @@ -6,7 +6,7 @@ import { isNonEmptyArray } from 'vs/base/common/arrays'; import { Barrier, CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { canceled, getErrorMessage } from 'vs/base/common/errors'; +import { CancellationError, getErrorMessage } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { isWeb } from 'vs/base/common/platform'; @@ -91,7 +91,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl const compatible = await this.checkAndGetCompatibleVersion(extension, !options.installGivenVersion, !!options.installPreReleaseVersion); return await this.installExtension(compatible.manifest, compatible.extension, options); } catch (error) { - reportTelemetry(this.telemetryService, 'extensionGallery:install', getGalleryExtensionTelemetryData(extension), undefined, error); + reportTelemetry(this.telemetryService, 'extensionGallery:install', { extensionData: getGalleryExtensionTelemetryData(extension), error }); this.logService.error(`Failed to install extension.`, extension.identifier.id); this.logService.error(error); throw toExtensionManagementError(error); @@ -216,7 +216,12 @@ export abstract class AbstractExtensionManagementService extends Disposable impl const local = await task.run(); await this.joinAllSettled(this.participants.map(participant => participant.postInstall(local, task.source, options, CancellationToken.None))); if (!URI.isUri(task.source)) { - reportTelemetry(this.telemetryService, task.operation === InstallOperation.Update ? 'extensionGallery:update' : 'extensionGallery:install', getGalleryExtensionTelemetryData(task.source), new Date().getTime() - startTime, undefined); + const isUpdate = task.operation === InstallOperation.Update; + reportTelemetry(this.telemetryService, isUpdate ? 'extensionGallery:update' : 'extensionGallery:install', { + extensionData: getGalleryExtensionTelemetryData(task.source), + duration: new Date().getTime() - startTime, + durationSinceUpdate: isUpdate ? undefined : new Date().getTime() - task.source.lastUpdated + }); // In web, report extension install statistics explicitly. In Desktop, statistics are automatically updated while downloading the VSIX. if (isWeb && task.operation !== InstallOperation.Update) { try { @@ -227,7 +232,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl installResults.push({ local, identifier: task.identifier, operation: task.operation, source: task.source }); } catch (error) { if (!URI.isUri(task.source)) { - reportTelemetry(this.telemetryService, task.operation === InstallOperation.Update ? 'extensionGallery:update' : 'extensionGallery:install', getGalleryExtensionTelemetryData(task.source), new Date().getTime() - startTime, error); + reportTelemetry(this.telemetryService, task.operation === InstallOperation.Update ? 'extensionGallery:update' : 'extensionGallery:install', { extensionData: getGalleryExtensionTelemetryData(task.source), duration: new Date().getTime() - startTime, error }); } this.logService.error('Error while installing the extension:', task.identifier.id); throw error; @@ -433,7 +438,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl } else { this.logService.info('Successfully uninstalled extension:', extension.identifier.id); } - reportTelemetry(this.telemetryService, 'extensionGallery:uninstall', getLocalExtensionTelemetryData(extension), undefined, error); + reportTelemetry(this.telemetryService, 'extensionGallery:uninstall', { extensionData: getLocalExtensionTelemetryData(extension), error }); this._onDidUninstallExtension.fire({ identifier: extension.identifier, error: error?.code }); }; @@ -611,12 +616,13 @@ function toExtensionManagementError(error: Error): ExtensionManagementError { return e; } -export function reportTelemetry(telemetryService: ITelemetryService, eventName: string, extensionData: any, duration?: number, error?: Error): void { +export function reportTelemetry(telemetryService: ITelemetryService, eventName: string, { extensionData, duration, error, durationSinceUpdate }: { extensionData: any; duration?: number; durationSinceUpdate?: number; error?: Error }): void { const errorcode = error ? error instanceof ExtensionManagementError ? error.code : ExtensionManagementErrorCode.Internal : undefined; /* __GDPR__ "extensionGallery:install" : { "success": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, "duration" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "durationSinceUpdate" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "errorcode": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, "recommendationReason": { "retiredFromVersion": "1.23.0", "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "${include}": [ @@ -644,7 +650,7 @@ export function reportTelemetry(telemetryService: ITelemetryService, eventName: ] } */ - telemetryService.publicLogError(eventName, { ...extensionData, success: !error, duration, errorcode }); + telemetryService.publicLog(eventName, { ...extensionData, success: !error, duration, errorcode, durationSinceUpdate }); } export abstract class AbstractExtensionTask<T> { @@ -671,7 +677,7 @@ export abstract class AbstractExtensionTask<T> { return new Promise((c, e) => { const disposable = token.onCancellationRequested(() => { disposable.dispose(); - e(canceled()); + e(new CancellationError()); }); }); }); diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index b83b399fd22..a3a7c9b84cd 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -23,7 +23,7 @@ import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator' import { IFileService } from 'vs/platform/files/common/files'; import { ILogService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; -import { asJson, asText, IRequestService, isSuccess } from 'vs/platform/request/common/request'; +import { asJson, asTextOrError, IRequestService, isSuccess } from 'vs/platform/request/common/request'; import { resolveMarketplaceHeaders } from 'vs/platform/externalServices/common/marketplace'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -221,6 +221,7 @@ interface IQueryState { readonly flags: Flags; readonly criteria: ICriterium[]; readonly assetTypes: string[]; + readonly source?: string; } const DefaultQueryState: IQueryState = { @@ -246,6 +247,7 @@ type GalleryServiceQueryClassification = { readonly statusCode?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; readonly errorCode?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; readonly count?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; + readonly source?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; }; type QueryTelemetryData = { @@ -254,6 +256,7 @@ type QueryTelemetryData = { readonly sortBy: string; readonly sortOrder: string; readonly pageNumber: string; + readonly source?: string; }; type GalleryServiceQueryEvent = QueryTelemetryData & { @@ -322,6 +325,10 @@ class Query { return new Query({ ...this.state, assetTypes }); } + withSource(source: string): Query { + return new Query({ ...this.state, source }); + } + get raw(): any { const { criteria, pageNumber, pageSize, sortBy, sortOrder, flags, assetTypes } = this.state; const filters = [{ criteria, pageNumber, pageSize, sortBy, sortOrder }]; @@ -339,7 +346,8 @@ class Query { flags: this.state.flags, sortBy: String(this.sortBy), sortOrder: String(this.sortOrder), - pageNumber: String(this.pageNumber) + pageNumber: String(this.pageNumber), + source: this.state.source }; } } @@ -600,6 +608,9 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi if (options.queryAllVersions || isQueryForReleaseVersionFromPreReleaseVersion /* Inlcude all versions if every requested extension is for release version and has pre-release version */) { query = query.withFlags(query.flags, Flags.IncludeVersions); } + if (options.source) { + query = query.withSource(options.source); + } const { extensions } = await this.queryGalleryExtensions(query, { targetPlatform: options.targetPlatform ?? CURRENT_TARGET_PLATFORM, includePreRelease: includePreReleases, versions, compatible: !!options.compatible }, token); if (options.source) { @@ -717,6 +728,10 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi query = query.withSortOrder(options.sortOrder); } + if (options.source) { + query = query.withSource(options.source); + } + const runQuery = async (query: Query, token: CancellationToken) => { const { extensions, total } = await this.queryGalleryExtensions(query, { targetPlatform: CURRENT_TARGET_PLATFORM, compatible: false, includePreRelease: !!options.includePreRelease }, token); extensions.forEach((e, index) => setTelemetry(e, ((query.pageNumber - 1) * query.pageSize) + index, options.source)); @@ -980,7 +995,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi async getReadme(extension: IGalleryExtension, token: CancellationToken): Promise<string> { if (extension.assets.readme) { const context = await this.getAsset(extension.assets.readme, {}, token); - const content = await asText(context); + const content = await asTextOrError(context); return content || ''; } return ''; @@ -989,7 +1004,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi async getManifest(extension: IGalleryExtension, token: CancellationToken): Promise<IExtensionManifest | null> { if (extension.assets.manifest) { const context = await this.getAsset(extension.assets.manifest, {}, token); - const text = await asText(context); + const text = await asTextOrError(context); return text ? JSON.parse(text) : null; } return null; @@ -1009,7 +1024,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi const asset = extension.assets.coreTranslations.filter(t => t[0] === languageId.toUpperCase())[0]; if (asset) { const context = await this.getAsset(asset[1]); - const text = await asText(context); + const text = await asTextOrError(context); return text ? JSON.parse(text) : null; } return null; @@ -1018,7 +1033,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi async getChangelog(extension: IGalleryExtension, token: CancellationToken): Promise<string> { if (extension.assets.changelog) { const context = await this.getAsset(extension.assets.changelog, {}, token); - const content = await asText(context); + const content = await asTextOrError(context); return content || ''; } return ''; @@ -1081,7 +1096,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi if (context.res.statusCode === 200) { return context; } - const message = await asText(context); + const message = await asTextOrError(context); throw new Error(`Expected 200, got back ${context.res.statusCode} instead.\n\n${message}`); } catch (err) { if (isCancellationError(err)) { diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index e7bf250146b..b28421b4bf1 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -398,7 +398,7 @@ export interface IExtensionManagementService { installFromGallery(extension: IGalleryExtension, options?: InstallOptions): Promise<ILocalExtension>; uninstall(extension: ILocalExtension, options?: UninstallOptions): Promise<void>; reinstallFromGallery(extension: ILocalExtension): Promise<void>; - getInstalled(type?: ExtensionType, donotIgnoreInvalidExtensions?: boolean): Promise<ILocalExtension[]>; + getInstalled(type?: ExtensionType): Promise<ILocalExtension[]>; getExtensionsControlManifest(): Promise<IExtensionsControlManifest>; updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata): Promise<ILocalExtension>; diff --git a/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts b/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts index 236b6e63174..5c6f4380295 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts @@ -27,9 +27,9 @@ const ExtensionKeyRegex = /^([^.]+\..+)-(\d+\.\d+\.\d+)(-(.+))?$/; export class ExtensionKey { - static create(extension: ILocalExtension | IGalleryExtension): ExtensionKey { - const version = (extension as ILocalExtension).manifest ? (extension as ILocalExtension).manifest.version : (extension as IGalleryExtension).version; - const targetPlatform = (extension as ILocalExtension).manifest ? (extension as ILocalExtension).targetPlatform : (extension as IGalleryExtension).properties.targetPlatform; + static create(extension: IExtension | IGalleryExtension): ExtensionKey { + const version = (extension as IExtension).manifest ? (extension as IExtension).manifest.version : (extension as IGalleryExtension).version; + const targetPlatform = (extension as IExtension).manifest ? (extension as IExtension).targetPlatform : (extension as IGalleryExtension).properties.targetPlatform; return new ExtensionKey(extension.identifier, version, targetPlatform); } diff --git a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts new file mode 100644 index 00000000000..93936e42335 --- /dev/null +++ b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts @@ -0,0 +1,875 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { coalesce } from 'vs/base/common/arrays'; +import { ThrottledDelayer } from 'vs/base/common/async'; +import * as objects from 'vs/base/common/objects'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { IStringDictionary } from 'vs/base/common/collections'; +import { getErrorMessage } from 'vs/base/common/errors'; +import { getNodeType, parse, ParseError } from 'vs/base/common/json'; +import { getParseErrorMessage } from 'vs/base/common/jsonErrorMessages'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { FileAccess, Schemas } from 'vs/base/common/network'; +import * as path from 'vs/base/common/path'; +import * as platform from 'vs/base/common/platform'; +import { basename, isEqual, joinPath } from 'vs/base/common/resources'; +import * as semver from 'vs/base/common/semver/semver'; +import Severity from 'vs/base/common/severity'; +import { isArray, isObject, isString } from 'vs/base/common/types'; +import { URI } from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { Metadata } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { areSameExtensions, computeTargetPlatform, ExtensionKey, getExtensionId, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { ExtensionType, ExtensionIdentifier, IExtensionManifest, TargetPlatform, IExtensionIdentifier, IRelaxedExtensionManifest, UNDEFINED_PUBLISHER, IExtensionDescription, BUILTIN_MANIFEST_CACHE_FILE, USER_MANIFEST_CACHE_FILE, MANIFEST_CACHE_FOLDER } from 'vs/platform/extensions/common/extensions'; +import { validateExtensionManifest } from 'vs/platform/extensions/common/extensionValidator'; +import { FileOperationResult, IFileService, toFileOperationResult } from 'vs/platform/files/common/files'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { Emitter, Event } from 'vs/base/common/event'; +import { revive } from 'vs/base/common/marshalling'; + +export type IScannedExtensionManifest = IRelaxedExtensionManifest & { __metadata?: Metadata }; + +interface IRelaxedScannedExtension { + type: ExtensionType; + isBuiltin: boolean; + identifier: IExtensionIdentifier; + manifest: IRelaxedExtensionManifest; + location: URI; + targetPlatform: TargetPlatform; + metadata: Metadata | undefined; + isValid: boolean; + validations: readonly [Severity, string][]; +} + +export type IScannedExtension = Readonly<IRelaxedScannedExtension> & { manifest: IExtensionManifest }; + +export interface Translations { + [id: string]: string; +} + +export namespace Translations { + export function equals(a: Translations, b: Translations): boolean { + if (a === b) { + return true; + } + let aKeys = Object.keys(a); + let bKeys: Set<string> = new Set<string>(); + for (let key of Object.keys(b)) { + bKeys.add(key); + } + if (aKeys.length !== bKeys.size) { + return false; + } + + for (let key of aKeys) { + if (a[key] !== b[key]) { + return false; + } + bKeys.delete(key); + } + return bKeys.size === 0; + } +} + +interface MessageBag { + [key: string]: string | { message: string; comment: string[] }; +} + +interface TranslationBundle { + contents: { + package: MessageBag; + }; +} + +interface LocalizedMessages { + values: MessageBag | undefined; + default: URI | null; +} + +interface IBuiltInExtensionControl { + [name: string]: 'marketplace' | 'disabled' | string; +} + +export type ScanOptions = { + readonly includeInvalid?: boolean; + readonly includeAllVersions?: boolean; + readonly includeUninstalled?: boolean; + readonly checkControlFile?: boolean; + readonly language?: string; + readonly useCache?: boolean; +}; + +export const IExtensionsScannerService = createDecorator<IExtensionsScannerService>('IExtensionsScannerService'); +export interface IExtensionsScannerService { + readonly _serviceBrand: undefined; + + readonly systemExtensionsLocation: URI; + readonly userExtensionsLocation: URI; + readonly onDidChangeCache: Event<ExtensionType>; + + getTargetPlatform(): Promise<TargetPlatform>; + + scanAllExtensions(scanOptions: ScanOptions): Promise<IScannedExtension[]>; + scanSystemExtensions(scanOptions: ScanOptions): Promise<IScannedExtension[]>; + scanUserExtensions(scanOptions: ScanOptions): Promise<IScannedExtension[]>; + scanExtensionsUnderDevelopment(scanOptions: ScanOptions, existingExtensions: IScannedExtension[]): Promise<IScannedExtension[]>; + scanExistingExtension(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise<IScannedExtension | null>; + scanOneOrMultipleExtensions(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise<IScannedExtension[]>; + + updateMetadata(extensionLocation: URI, metadata: Partial<Metadata>): Promise<void>; +} + +export abstract class AbstractExtensionsScannerService extends Disposable implements IExtensionsScannerService { + + readonly _serviceBrand: undefined; + + protected abstract getTranslations(language: string): Promise<Translations>; + + private readonly _onDidChangeCache = this._register(new Emitter<ExtensionType>()); + readonly onDidChangeCache = this._onDidChangeCache.event; + + private readonly systemExtensionsCachedScanner = this._register(new CachedExtensionsScanner(joinPath(this.cacheLocation, BUILTIN_MANIFEST_CACHE_FILE), this.fileService, this.logService)); + private readonly userExtensionsCachedScanner = this._register(new CachedExtensionsScanner(joinPath(this.cacheLocation, USER_MANIFEST_CACHE_FILE), this.fileService, this.logService)); + private readonly extensionsScanner = this._register(new ExtensionsScanner(this.fileService, this.logService)); + + constructor( + readonly systemExtensionsLocation: URI, + readonly userExtensionsLocation: URI, + private readonly extensionsControlLocation: URI, + private readonly cacheLocation: URI, + @IFileService protected readonly fileService: IFileService, + @ILogService protected readonly logService: ILogService, + @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IProductService private readonly productService: IProductService, + ) { + super(); + + this._register(this.systemExtensionsCachedScanner.onDidChangeCache(() => this._onDidChangeCache.fire(ExtensionType.System))); + this._register(this.userExtensionsCachedScanner.onDidChangeCache(() => this._onDidChangeCache.fire(ExtensionType.User))); + } + + private _targetPlatformPromise: Promise<TargetPlatform> | undefined; + getTargetPlatform(): Promise<TargetPlatform> { + if (!this._targetPlatformPromise) { + this._targetPlatformPromise = computeTargetPlatform(this.fileService, this.logService); + } + return this._targetPlatformPromise; + } + + async scanAllExtensions(scanOptions: ScanOptions): Promise<IScannedExtension[]> { + const [system, user] = await Promise.all([ + this.scanSystemExtensions(scanOptions), + this.scanUserExtensions(scanOptions), + ]); + const development = await this.scanExtensionsUnderDevelopment(scanOptions, [...system, ...user]); + return this.dedupExtensions([...system, ...user, ...development], await this.getTargetPlatform(), true); + } + + async scanSystemExtensions(scanOptions: ScanOptions): Promise<IScannedExtension[]> { + const promises: Promise<IRelaxedScannedExtension[]>[] = []; + promises.push(this.scanDefaultSystemExtensions(!!scanOptions.useCache, scanOptions.language)); + promises.push(this.scanDevSystemExtensions(scanOptions.language, !!scanOptions.checkControlFile)); + const [defaultSystemExtensions, devSystemExtensions] = await Promise.all(promises); + return this.applyScanOptions([...defaultSystemExtensions, ...devSystemExtensions], scanOptions, false); + } + + async scanUserExtensions(scanOptions: ScanOptions): Promise<IScannedExtension[]> { + this.logService.trace('Started scanning user extensions'); + const extensionsScannerInput = await this.createExtensionScannerInput(this.userExtensionsLocation, ExtensionType.User, !scanOptions.includeUninstalled, scanOptions.language); + const extensionsScanner = scanOptions.useCache && !extensionsScannerInput.devMode && extensionsScannerInput.excludeObsolete ? this.userExtensionsCachedScanner : this.extensionsScanner; + let extensions = await extensionsScanner.scanExtensions(extensionsScannerInput); + extensions = await this.applyScanOptions(extensions, scanOptions, true); + this.logService.trace('Scanned user extensions:', extensions.length); + return extensions; + } + + async scanExtensionsUnderDevelopment(scanOptions: ScanOptions, existingExtensions: IScannedExtension[]): Promise<IScannedExtension[]> { + if (this.environmentService.isExtensionDevelopment && this.environmentService.extensionDevelopmentLocationURI) { + const extensions = (await Promise.all(this.environmentService.extensionDevelopmentLocationURI.filter(extLoc => extLoc.scheme === Schemas.file) + .map(async extensionDevelopmentLocationURI => { + const input = await this.createExtensionScannerInput(extensionDevelopmentLocationURI, ExtensionType.User, true, scanOptions.language, false /* do not validate */); + const extensions = await this.extensionsScanner.scanOneOrMultipleExtensions(input); + return extensions.map(extension => { + // Override the extension type from the existing extensions + extension.type = existingExtensions.find(e => areSameExtensions(e.identifier, extension.identifier))?.type ?? extension.type; + // Validate the extension + return this.extensionsScanner.validate(extension, input); + }); + }))) + .flat(); + return this.applyScanOptions(extensions, scanOptions, true); + } + return []; + } + + async scanExistingExtension(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise<IScannedExtension | null> { + const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, extensionType, true, scanOptions.language); + const extension = await this.extensionsScanner.scanExtension(extensionsScannerInput); + if (!extension) { + return null; + } + if (!scanOptions.includeInvalid && !extension.isValid) { + return null; + } + return extension; + } + + async scanOneOrMultipleExtensions(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise<IScannedExtension[]> { + const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, extensionType, true, scanOptions.language); + const extensions = await this.extensionsScanner.scanOneOrMultipleExtensions(extensionsScannerInput); + return this.applyScanOptions(extensions, scanOptions, true); + } + + async updateMetadata(extensionLocation: URI, metaData: Partial<Metadata>): Promise<void> { + const manifestLocation = joinPath(extensionLocation, 'package.json'); + const content = (await this.fileService.readFile(manifestLocation)).value.toString(); + const manifest: IScannedExtensionManifest = JSON.parse(content); + + // unset if false + metaData.isMachineScoped = metaData.isMachineScoped || undefined; + metaData.isBuiltin = metaData.isBuiltin || undefined; + metaData.installedTimestamp = metaData.installedTimestamp || undefined; + manifest.__metadata = { ...manifest.__metadata, ...metaData }; + + await this.fileService.writeFile(joinPath(extensionLocation, 'package.json'), VSBuffer.fromString(JSON.stringify(manifest, null, '\t'))); + } + + private async applyScanOptions(extensions: IRelaxedScannedExtension[], scanOptions: ScanOptions, pickLatest: boolean): Promise<IRelaxedScannedExtension[]> { + if (!scanOptions.includeAllVersions) { + extensions = this.dedupExtensions(extensions, await this.getTargetPlatform(), pickLatest); + } + if (!scanOptions.includeInvalid) { + extensions = extensions.filter(extension => extension.isValid); + } + return extensions.sort((a, b) => { + const aLastSegment = path.basename(a.location.fsPath); + const bLastSegment = path.basename(b.location.fsPath); + if (aLastSegment < bLastSegment) { + return -1; + } + if (aLastSegment > bLastSegment) { + return 1; + } + return 0; + }); + } + + private dedupExtensions(extensions: IRelaxedScannedExtension[], targetPlatform: TargetPlatform, pickLatest: boolean): IRelaxedScannedExtension[] { + const result = new Map<string, IRelaxedScannedExtension>(); + for (const extension of extensions) { + const extensionKey = ExtensionIdentifier.toKey(extension.identifier.id); + const existing = result.get(extensionKey); + if (existing) { + if (existing.isValid && !extension.isValid) { + continue; + } + if (existing.isValid === extension.isValid) { + if (pickLatest && semver.gt(existing.manifest.version, extension.manifest.version)) { + this.logService.debug(`Skipping extension ${extension.location.path} with lower version ${extension.manifest.version}.`); + continue; + } + if (semver.eq(existing.manifest.version, extension.manifest.version) && existing.targetPlatform === targetPlatform) { + this.logService.debug(`Skipping extension ${extension.location.path} from different target platform ${extension.targetPlatform}`); + continue; + } + } + if (existing.type === ExtensionType.System) { + this.logService.debug(`Overwriting system extension ${existing.location.path} with ${extension.location.path}.`); + } else { + this.logService.warn(`Overwriting user extension ${existing.location.path} with ${extension.location.path}.`); + } + } + result.set(extensionKey, extension); + } + return [...result.values()]; + } + + private async scanDefaultSystemExtensions(useCache: boolean, language: string | undefined): Promise<IRelaxedScannedExtension[]> { + this.logService.trace('Started scanning system extensions'); + const extensionsScannerInput = await this.createExtensionScannerInput(this.systemExtensionsLocation, ExtensionType.System, true, language); + const extensionsScanner = useCache && !extensionsScannerInput.devMode ? this.systemExtensionsCachedScanner : this.extensionsScanner; + const result = await extensionsScanner.scanExtensions(extensionsScannerInput); + this.logService.trace('Scanned system extensions:', result.length); + return result; + } + + private async scanDevSystemExtensions(language: string | undefined, checkControlFile: boolean): Promise<IRelaxedScannedExtension[]> { + const devSystemExtensionsList = this.environmentService.isBuilt ? [] : this.productService.builtInExtensions; + if (!devSystemExtensionsList?.length) { + return []; + } + + this.logService.trace('Started scanning dev system extensions'); + const builtinExtensionControl = checkControlFile ? await this.getBuiltInExtensionControl() : {}; + const devSystemExtensionsLocations: URI[] = []; + const devSystemExtensionsLocation = URI.file(path.normalize(path.join(FileAccess.asFileUri('', require).fsPath, '..', '.build', 'builtInExtensions'))); + for (const extension of devSystemExtensionsList) { + const controlState = builtinExtensionControl[extension.name] || 'marketplace'; + switch (controlState) { + case 'disabled': + break; + case 'marketplace': + devSystemExtensionsLocations.push(joinPath(devSystemExtensionsLocation, extension.name)); + break; + default: + devSystemExtensionsLocations.push(URI.file(controlState)); + break; + } + } + const result = await Promise.all(devSystemExtensionsLocations.map(async location => this.extensionsScanner.scanExtension((await this.createExtensionScannerInput(location, ExtensionType.System, true, language))))); + this.logService.trace('Scanned dev system extensions:', result.length); + return coalesce(result); + } + + private async getBuiltInExtensionControl(): Promise<IBuiltInExtensionControl> { + try { + const content = await this.fileService.readFile(this.extensionsControlLocation); + return JSON.parse(content.value.toString()); + } catch (error) { + return {}; + } + } + + private async createExtensionScannerInput(location: URI, type: ExtensionType, excludeObsolete: boolean, language: string | undefined, validate: boolean = true): Promise<ExtensionScannerInput> { + const translations = await this.getTranslations(language ?? platform.language); + let mtime: number | undefined; + try { + const folderStat = await this.fileService.stat(location); + if (typeof folderStat.mtime === 'number') { + mtime = folderStat.mtime; + } + } catch (err) { + // That's ok... + } + return new ExtensionScannerInput( + location, + mtime, + type, + excludeObsolete, + validate, + this.productService.version, + this.productService.date, + this.productService.commit, + !this.environmentService.isBuilt, + language, + translations, + ); + } + +} + +class ExtensionScannerInput { + + constructor( + public readonly location: URI, + public readonly mtime: number | undefined, + public readonly type: ExtensionType, + public readonly excludeObsolete: boolean, + public readonly validate: boolean, + public readonly productVersion: string, + public readonly productDate: string | undefined, + public readonly productCommit: string | undefined, + public readonly devMode: boolean, + public readonly language: string | undefined, + public readonly translations: Translations + ) { + // Keep empty!! (JSON.parse) + } + + public static createNlsConfiguration(input: ExtensionScannerInput): NlsConfiguration { + return { + language: input.language, + pseudo: input.language === 'pseudo', + devMode: input.devMode, + translations: input.translations + }; + } + + public static equals(a: ExtensionScannerInput, b: ExtensionScannerInput): boolean { + return ( + isEqual(a.location, b.location) + && a.mtime === b.mtime + && a.type === b.type + && a.excludeObsolete === b.excludeObsolete + && a.validate === b.validate + && a.productVersion === b.productVersion + && a.productDate === b.productDate + && a.productCommit === b.productCommit + && a.devMode === b.devMode + && a.language === b.language + && Translations.equals(a.translations, b.translations) + ); + } +} + +type NlsConfiguration = { + language: string | undefined; + pseudo: boolean; + devMode: boolean; + translations: Translations; +}; + +class ExtensionsScanner extends Disposable { + + constructor( + protected readonly fileService: IFileService, + protected readonly logService: ILogService + ) { + super(); + } + + async scanExtensions(input: ExtensionScannerInput): Promise<IRelaxedScannedExtension[]> { + const stat = await this.fileService.resolve(input.location); + if (stat.children) { + let obsolete: IStringDictionary<boolean> = {}; + if (input.excludeObsolete && input.type === ExtensionType.User) { + try { + const raw = (await this.fileService.readFile(joinPath(input.location, '.obsolete'))).value.toString(); + obsolete = JSON.parse(raw); + } catch (error) { /* ignore */ } + } + const extensions = await Promise.all<IRelaxedScannedExtension | null>( + stat.children.map(async c => { + if (!c.isDirectory) { + return null; + } + // Do not consider user extension folder starting with `.` + if (input.type === ExtensionType.User && basename(c.resource).indexOf('.') === 0) { + return null; + } + const extensionScannerInput = new ExtensionScannerInput(c.resource, input.mtime, input.type, input.excludeObsolete, input.validate, input.productVersion, input.productDate, input.productCommit, input.devMode, input.language, input.translations); + const extension = await this.scanExtension(extensionScannerInput); + return extension && !obsolete[ExtensionKey.create(extension).toString()] ? extension : null; + })); + return coalesce(extensions); + } + return []; + } + + async scanOneOrMultipleExtensions(input: ExtensionScannerInput): Promise<IRelaxedScannedExtension[]> { + try { + if (await this.fileService.exists(joinPath(input.location, 'package.json'))) { + const extension = await this.scanExtension(input); + return extension ? [extension] : []; + } else { + return await this.scanExtensions(input); + } + } catch (error) { + this.logService.error(`Error scanning extensions at ${input.location.path}:`, getErrorMessage(error)); + return []; + } + } + + async scanExtension(input: ExtensionScannerInput): Promise<IRelaxedScannedExtension | null> { + try { + let manifest = await this.scanExtensionManifest(input.location); + if (manifest) { + // allow publisher to be undefined to make the initial extension authoring experience smoother + if (!manifest.publisher) { + manifest.publisher = UNDEFINED_PUBLISHER; + } + const metadata = manifest.__metadata; + delete manifest.__metadata; + const id = getGalleryExtensionId(manifest.publisher, manifest.name); + const identifier = metadata?.id ? { id, uuid: metadata.id } : { id }; + const type = metadata?.isSystem ? ExtensionType.System : input.type; + const isBuiltin = type === ExtensionType.System || !!metadata?.isBuiltin; + manifest = await this.translateManifest(input.location, manifest, ExtensionScannerInput.createNlsConfiguration(input)); + const extension = { + type, + identifier, + manifest, + location: input.location, + isBuiltin, + targetPlatform: metadata?.targetPlatform ?? TargetPlatform.UNDEFINED, + metadata, + isValid: true, + validations: [] + }; + return input.validate ? this.validate(extension, input) : extension; + } + } catch (e) { + if (input.type !== ExtensionType.System) { + this.logService.error(e); + } + } + return null; + } + + validate(extension: IRelaxedScannedExtension, input: ExtensionScannerInput): IRelaxedScannedExtension { + let isValid = true; + const validations = validateExtensionManifest(input.productVersion, input.productDate, input.location, extension.manifest, extension.isBuiltin); + for (const [severity, message] of validations) { + if (severity === Severity.Error) { + isValid = false; + this.logService.error(this.formatMessage(input.location, message)); + } + } + extension.isValid = isValid; + extension.validations = validations; + return extension; + } + + private async scanExtensionManifest(extensionLocation: URI): Promise<IScannedExtensionManifest | null> { + const manifestLocation = joinPath(extensionLocation, 'package.json'); + let content; + try { + content = (await this.fileService.readFile(manifestLocation)).value.toString(); + } catch (error) { + if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) { + this.logService.error(this.formatMessage(extensionLocation, localize('fileReadFail', "Cannot read file {0}: {1}.", manifestLocation.path, error.message))); + } + return null; + } + let manifest: IScannedExtensionManifest; + try { + manifest = JSON.parse(content); + } catch (err) { + // invalid JSON, let's get good errors + const errors: ParseError[] = []; + parse(content, errors); + for (const e of errors) { + this.logService.error(this.formatMessage(extensionLocation, localize('jsonParseFail', "Failed to parse {0}: [{1}, {2}] {3}.", manifestLocation.path, e.offset, e.length, getParseErrorMessage(e.error)))); + } + return null; + } + if (getNodeType(manifest) !== 'object') { + this.logService.error(this.formatMessage(extensionLocation, localize('jsonParseInvalidType', "Invalid manifest file {0}: Not an JSON object.", manifestLocation.path))); + return null; + } + return manifest; + } + + private async translateManifest(extensionLocation: URI, extensionManifest: IExtensionManifest, nlsConfiguration: NlsConfiguration): Promise<IExtensionManifest> { + const localizedMessages = await this.getLocalizedMessages(extensionLocation, extensionManifest, nlsConfiguration); + if (localizedMessages) { + try { + const errors: ParseError[] = []; + // resolveOriginalMessageBundle returns null if localizedMessages.default === undefined; + const defaults = await this.resolveOriginalMessageBundle(localizedMessages.default, errors); + if (errors.length > 0) { + errors.forEach((error) => { + this.logService.error(this.formatMessage(extensionLocation, localize('jsonsParseReportErrors', "Failed to parse {0}: {1}.", localizedMessages.default?.path, getParseErrorMessage(error.error)))); + }); + return extensionManifest; + } else if (getNodeType(localizedMessages) !== 'object') { + this.logService.error(this.formatMessage(extensionLocation, localize('jsonInvalidFormat', "Invalid format {0}: JSON object expected.", localizedMessages.default?.path))); + return extensionManifest; + } + const localized = localizedMessages.values || Object.create(null); + this.replaceNLStrings(nlsConfiguration.pseudo, extensionManifest, localized, defaults, extensionLocation); + } catch (error) { + /*Ignore Error*/ + } + } + return extensionManifest; + } + + private async getLocalizedMessages(extensionLocation: URI, extensionManifest: IExtensionManifest, nlsConfiguration: NlsConfiguration): Promise<LocalizedMessages | undefined> { + const defaultPackageNLS = joinPath(extensionLocation, 'package.nls.json'); + const reportErrors = (localized: URI | null, errors: ParseError[]): void => { + errors.forEach((error) => { + this.logService.error(this.formatMessage(extensionLocation, localize('jsonsParseReportErrors', "Failed to parse {0}: {1}.", localized?.path, getParseErrorMessage(error.error)))); + }); + }; + const reportInvalidFormat = (localized: URI | null): void => { + this.logService.error(this.formatMessage(extensionLocation, localize('jsonInvalidFormat', "Invalid format {0}: JSON object expected.", localized?.path))); + }; + + const translationId = `${extensionManifest.publisher}.${extensionManifest.name}`; + const translationPath = nlsConfiguration.translations[translationId]; + + if (translationPath) { + try { + const translationResource = URI.file(translationPath); + const content = (await this.fileService.readFile(translationResource)).value.toString(); + let errors: ParseError[] = []; + let translationBundle: TranslationBundle = parse(content, errors); + if (errors.length > 0) { + reportErrors(translationResource, errors); + return { values: undefined, default: defaultPackageNLS }; + } else if (getNodeType(translationBundle) !== 'object') { + reportInvalidFormat(translationResource); + return { values: undefined, default: defaultPackageNLS }; + } else { + let values = translationBundle.contents ? translationBundle.contents.package : undefined; + return { values: values, default: defaultPackageNLS }; + } + } catch (error) { + return { values: undefined, default: defaultPackageNLS }; + } + } else { + const exists = await this.fileService.exists(defaultPackageNLS); + if (!exists) { + return undefined; + } + let messageBundle; + try { + messageBundle = await this.findMessageBundles(extensionLocation, nlsConfiguration); + } catch (error) { + return undefined; + } + if (!messageBundle.localized) { + return { values: undefined, default: messageBundle.original }; + } + try { + const messageBundleContent = (await this.fileService.readFile(messageBundle.localized)).value.toString(); + let errors: ParseError[] = []; + let messages: MessageBag = parse(messageBundleContent, errors); + if (errors.length > 0) { + reportErrors(messageBundle.localized, errors); + return { values: undefined, default: messageBundle.original }; + } else if (getNodeType(messages) !== 'object') { + reportInvalidFormat(messageBundle.localized); + return { values: undefined, default: messageBundle.original }; + } + return { values: messages, default: messageBundle.original }; + } catch (error) { + return { values: undefined, default: messageBundle.original }; + } + } + } + + /** + * Parses original message bundle, returns null if the original message bundle is null. + */ + private async resolveOriginalMessageBundle(originalMessageBundle: URI | null, errors: ParseError[]): Promise<{ [key: string]: string } | null> { + if (originalMessageBundle) { + try { + const originalBundleContent = (await this.fileService.readFile(originalMessageBundle)).value.toString(); + return parse(originalBundleContent, errors); + } catch (error) { + /* Ignore Error */ + return null; + } + } else { + return null; + } + } + + /** + * Finds localized message bundle and the original (unlocalized) one. + * If the localized file is not present, returns null for the original and marks original as localized. + */ + private findMessageBundles(extensionLocation: URI, nlsConfiguration: NlsConfiguration): Promise<{ localized: URI; original: URI | null }> { + return new Promise<{ localized: URI; original: URI | null }>((c, e) => { + const loop = (locale: string): void => { + let toCheck = joinPath(extensionLocation, `package.nls.${locale}.json`); + this.fileService.exists(toCheck).then(exists => { + if (exists) { + c({ localized: toCheck, original: joinPath(extensionLocation, 'package.nls.json') }); + } + let index = locale.lastIndexOf('-'); + if (index === -1) { + c({ localized: joinPath(extensionLocation, 'package.nls.json'), original: null }); + } else { + locale = locale.substring(0, index); + loop(locale); + } + }); + }; + if (nlsConfiguration.devMode || nlsConfiguration.pseudo || !nlsConfiguration.language) { + return c({ localized: joinPath(extensionLocation, 'package.nls.json'), original: null }); + } + loop(nlsConfiguration.language); + }); + } + + /** + * This routine makes the following assumptions: + * The root element is an object literal + */ + private replaceNLStrings<T extends object>(pseudo: boolean, literal: T, messages: MessageBag, originalMessages: MessageBag | null, extensionLocation: URI): void { + const processEntry = (obj: any, key: string | number, command?: boolean) => { + const value = obj[key]; + if (isString(value)) { + const str = <string>value; + const length = str.length; + if (length > 1 && str[0] === '%' && str[length - 1] === '%') { + const messageKey = str.substr(1, length - 2); + let translated = messages[messageKey]; + // If the messages come from a language pack they might miss some keys + // Fill them from the original messages. + if (translated === undefined && originalMessages) { + translated = originalMessages[messageKey]; + } + let message: string | undefined = typeof translated === 'string' ? translated : (typeof translated?.message === 'string' ? translated.message : undefined); + if (message !== undefined) { + if (pseudo) { + // FF3B and FF3D is the Unicode zenkaku representation for [ and ] + message = '\uFF3B' + message.replace(/[aouei]/g, '$&$&') + '\uFF3D'; + } + obj[key] = command && (key === 'title' || key === 'category') && originalMessages ? { value: message, original: originalMessages[messageKey] } : message; + } else { + this.logService.warn(this.formatMessage(extensionLocation, localize('missingNLSKey', "Couldn't find message for key {0}.", messageKey))); + } + } + } else if (isObject(value)) { + for (let k in value) { + if (value.hasOwnProperty(k)) { + k === 'commands' ? processEntry(value, k, true) : processEntry(value, k, command); + } + } + } else if (isArray(value)) { + for (let i = 0; i < value.length; i++) { + processEntry(value, i, command); + } + } + }; + + for (let key in literal) { + if (literal.hasOwnProperty(key)) { + processEntry(literal, key); + } + } + } + + private formatMessage(extensionLocation: URI, message: string): string { + return `[${extensionLocation.path}]: ${message}`; + } + +} + +interface IExtensionCacheData { + input: ExtensionScannerInput; + result: IRelaxedScannedExtension[]; +} + +class CachedExtensionsScanner extends ExtensionsScanner { + + private input: ExtensionScannerInput | undefined; + private readonly cacheValidatorThrottler: ThrottledDelayer<void> = this._register(new ThrottledDelayer(3000)); + + private readonly _onDidChangeCache = this._register(new Emitter<void>()); + readonly onDidChangeCache = this._onDidChangeCache.event; + + constructor( + private readonly cacheFile: URI, + fileService: IFileService, + logService: ILogService + ) { + super(fileService, logService); + } + + override async scanExtensions(input: ExtensionScannerInput): Promise<IRelaxedScannedExtension[]> { + const cacheContents = await this.readExtensionCache(); + this.input = input; + if (cacheContents && cacheContents.input && ExtensionScannerInput.equals(cacheContents.input, this.input)) { + this.cacheValidatorThrottler.trigger(() => this.validateCache()); + return cacheContents.result.map((extension) => { + // revive URI object + extension.location = URI.revive(extension.location); + return extension; + }); + } + const result = await super.scanExtensions(input); + await this.writeExtensionCache({ input, result }); + return result; + } + + private async readExtensionCache(): Promise<IExtensionCacheData | null> { + try { + const cacheRawContents = await this.fileService.readFile(this.cacheFile); + const extensionCacheData: IExtensionCacheData = JSON.parse(cacheRawContents.value.toString()); + return { result: extensionCacheData.result, input: revive(extensionCacheData.input) }; + } catch (error) { + this.logService.debug('Error while reading the extension cache file:', this.cacheFile.path, getErrorMessage(error)); + } + return null; + } + + private async writeExtensionCache(cacheContents: IExtensionCacheData): Promise<void> { + try { + await this.fileService.writeFile(this.cacheFile, VSBuffer.fromString(JSON.stringify(cacheContents))); + } catch (error) { + this.logService.debug('Error while writing the extension cache file:', this.cacheFile.path, getErrorMessage(error)); + } + } + + private async validateCache(): Promise<void> { + if (!this.input) { + // Input has been unset by the time we get here, so skip validation + return; + } + + const cacheContents = await this.readExtensionCache(); + if (!cacheContents) { + // Cache has been deleted by someone else, which is perfectly fine... + return; + } + + const actual = cacheContents.result; + const expected = JSON.parse(JSON.stringify(await super.scanExtensions(this.input))); + if (objects.equals(expected, actual)) { + // Cache is valid and running with it is perfectly fine... + return; + } + + try { + // Cache is invalid, delete it + await this.fileService.del(this.cacheFile); + this._onDidChangeCache.fire(); + } catch (error) { + this.logService.error(error); + } + } + +} + +export function toExtensionDescription(extension: IScannedExtension, isUnderDevelopment: boolean): IExtensionDescription { + const id = getExtensionId(extension.manifest.publisher, extension.manifest.name); + return { + id, + identifier: new ExtensionIdentifier(id), + isBuiltin: extension.type === ExtensionType.System, + isUserBuiltin: extension.type === ExtensionType.User && extension.isBuiltin, + isUnderDevelopment, + extensionLocation: extension.location, + uuid: extension.identifier.uuid, + targetPlatform: extension.targetPlatform, + ...extension.manifest, + }; +} + +export class NativeExtensionsScannerService extends AbstractExtensionsScannerService implements IExtensionsScannerService { + + private readonly translationsPromise: Promise<Translations>; + + constructor( + systemExtensionsLocation: URI, + userExtensionsLocation: URI, + userHome: URI, + userDataPath: URI, + fileService: IFileService, + logService: ILogService, + environmentService: IEnvironmentService, + productService: IProductService, + ) { + super( + systemExtensionsLocation, + userExtensionsLocation, + joinPath(userHome, '.vscode-oss-dev', 'extensions', 'control.json'), + joinPath(userDataPath, MANIFEST_CACHE_FOLDER), + fileService, logService, environmentService, productService); + this.translationsPromise = (async () => { + if (platform.translationsConfigFile) { + try { + const content = await this.fileService.readFile(URI.file(platform.translationsConfigFile)); + return JSON.parse(content.value.toString()); + } catch (err) { /* Ignore Error */ } + } + return Object.create(null); + })(); + } + + protected getTranslations(language: string): Promise<Translations> { + return this.translationsPromise; + } + +} diff --git a/src/vs/platform/extensionManagement/electron-sandbox/extensionsScannerService.ts b/src/vs/platform/extensionManagement/electron-sandbox/extensionsScannerService.ts new file mode 100644 index 00000000000..7914d9aff79 --- /dev/null +++ b/src/vs/platform/extensionManagement/electron-sandbox/extensionsScannerService.ts @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IExtensionsScannerService, NativeExtensionsScannerService, } from 'vs/platform/extensionManagement/common/extensionsScannerService'; +import { IFileService } from 'vs/platform/files/common/files'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IProductService } from 'vs/platform/product/common/productService'; + +export class ExtensionsScannerService extends NativeExtensionsScannerService implements IExtensionsScannerService { + + constructor( + @IFileService fileService: IFileService, + @ILogService logService: ILogService, + @INativeEnvironmentService environmentService: INativeEnvironmentService, + @IProductService productService: IProductService, + ) { + super( + URI.file(environmentService.builtinExtensionsPath), + URI.file(environmentService.extensionsPath), + environmentService.userHome, + URI.file(environmentService.userDataPath), + fileService, logService, environmentService, productService); + } + +} + +registerSingleton(IExtensionsScannerService, ExtensionsScannerService); diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 0a0254ea806..2e52feaa54f 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -3,19 +3,22 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Promises, Queue } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { IStringDictionary } from 'vs/base/common/collections'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { getErrorMessage } from 'vs/base/common/errors'; +import { Disposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import * as path from 'vs/base/common/path'; -import { isMacintosh } from 'vs/base/common/platform'; +import { isMacintosh, isWindows } from 'vs/base/common/platform'; import { joinPath } from 'vs/base/common/resources'; import * as semver from 'vs/base/common/semver/semver'; import { isBoolean, isUndefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import * as pfs from 'vs/base/node/pfs'; -import { IFile, zip } from 'vs/base/node/zip'; +import { extract, ExtractError, IFile, zip } from 'vs/base/node/zip'; import * as nls from 'vs/nls'; import { IDownloadService } from 'vs/platform/download/common/download'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -24,12 +27,12 @@ import { ExtensionManagementError, ExtensionManagementErrorCode, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementService, IGalleryExtension, IGalleryMetadata, ILocalExtension, InstallOperation, InstallOptions, InstallVSIXOptions, Metadata } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { areSameExtensions, computeTargetPlatform, ExtensionKey, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { areSameExtensions, computeTargetPlatform, ExtensionKey, getGalleryExtensionId, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { IExtensionsScannerService, IScannedExtension, ScanOptions } from 'vs/platform/extensionManagement/common/extensionsScannerService'; import { ExtensionsDownloader } from 'vs/platform/extensionManagement/node/extensionDownloader'; import { ExtensionsLifecycle } from 'vs/platform/extensionManagement/node/extensionLifecycle'; import { getManifest } from 'vs/platform/extensionManagement/node/extensionManagementUtil'; import { ExtensionsManifestCache } from 'vs/platform/extensionManagement/node/extensionsManifestCache'; -import { ExtensionsScanner, ILocalExtensionManifest } from 'vs/platform/extensionManagement/node/extensionsScanner'; import { ExtensionsWatcher } from 'vs/platform/extensionManagement/node/extensionsWatcher'; import { ExtensionType, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator'; @@ -65,7 +68,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi ) { super(galleryService, telemetryService, logService, productService); const extensionLifecycle = this._register(instantiationService.createInstance(ExtensionsLifecycle)); - this.extensionsScanner = this._register(instantiationService.createInstance(ExtensionsScanner, extension => extensionLifecycle.postUninstall(extension), this.getTargetPlatform())); + this.extensionsScanner = this._register(instantiationService.createInstance(ExtensionsScanner, extension => extensionLifecycle.postUninstall(extension))); this.manifestCache = this._register(new ExtensionsManifestCache(environmentService, this)); this.extensionsDownloader = this._register(instantiationService.createInstance(ExtensionsDownloader)); const extensionsWatcher = this._register(new ExtensionsWatcher(this, fileService, environmentService, logService, uriIdentityService)); @@ -123,18 +126,18 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi async updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata): Promise<ILocalExtension> { this.logService.trace('ExtensionManagementService#updateMetadata', local.identifier.id); - const localMetadata: Metadata = { ...((<ILocalExtensionManifest>local.manifest).__metadata || {}), ...metadata }; + const localMetadata: Metadata = { ...metadata }; if (metadata.isPreReleaseVersion) { localMetadata.preRelease = true; } - local = await this.extensionsScanner.saveMetadataForLocalExtension(local, localMetadata); + local = await this.extensionsScanner.updateMetadata(local, localMetadata); this.manifestCache.invalidate(); return local; } async updateExtensionScope(local: ILocalExtension, isMachineScoped: boolean): Promise<ILocalExtension> { this.logService.trace('ExtensionManagementService#updateExtensionScope', local.identifier.id); - local = await this.extensionsScanner.saveMetadataForLocalExtension(local, { ...((<ILocalExtensionManifest>local.manifest).__metadata || {}), isMachineScoped }); + local = await this.extensionsScanner.updateMetadata(local, { isMachineScoped }); this.manifestCache.invalidate(); return local; } @@ -187,6 +190,272 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi } +class ExtensionsScanner extends Disposable { + + private readonly uninstalledPath: string; + private readonly uninstalledFileLimiter: Queue<any>; + + constructor( + private readonly beforeRemovingExtension: (e: ILocalExtension) => Promise<void>, + @IFileService private readonly fileService: IFileService, + @IExtensionsScannerService private readonly extensionsScannerService: IExtensionsScannerService, + @ILogService private readonly logService: ILogService, + ) { + super(); + this.uninstalledPath = joinPath(this.extensionsScannerService.userExtensionsLocation, '.obsolete').fsPath; + this.uninstalledFileLimiter = new Queue(); + } + + async cleanUp(): Promise<void> { + await this.removeUninstalledExtensions(); + await this.removeOutdatedExtensions(); + } + + async scanExtensions(type: ExtensionType | null): Promise<ILocalExtension[]> { + const scannedOptions: ScanOptions = { includeInvalid: true }; + let scannedExtensions: IScannedExtension[] = []; + if (type === null || type === ExtensionType.System) { + scannedExtensions.push(...await this.extensionsScannerService.scanAllExtensions(scannedOptions)); + } else if (type === ExtensionType.User) { + scannedExtensions.push(...await this.extensionsScannerService.scanUserExtensions(scannedOptions)); + } + scannedExtensions = type !== null ? scannedExtensions.filter(r => r.type === type) : scannedExtensions; + return Promise.all(scannedExtensions.map(extension => this.toLocalExtension(extension))); + } + + async scanUserExtensions(excludeOutdated: boolean): Promise<ILocalExtension[]> { + const scannedExtensions = await this.extensionsScannerService.scanUserExtensions({ includeAllVersions: !excludeOutdated, includeInvalid: true }); + return Promise.all(scannedExtensions.map(extension => this.toLocalExtension(extension))); + } + + async extractUserExtension(extensionKey: ExtensionKey, zipPath: string, metadata: Metadata | undefined, token: CancellationToken): Promise<ILocalExtension> { + const folderName = extensionKey.toString(); + const tempPath = path.join(this.extensionsScannerService.userExtensionsLocation.fsPath, `.${generateUuid()}`); + const extensionPath = path.join(this.extensionsScannerService.userExtensionsLocation.fsPath, folderName); + + try { + await pfs.Promises.rm(extensionPath); + } catch (error) { + throw new ExtensionManagementError(nls.localize('errorDeleting', "Unable to delete the existing folder '{0}' while installing the extension '{1}'. Please delete the folder manually and try again", extensionPath, extensionKey.id), ExtensionManagementErrorCode.Delete); + } + + await this.extractAtLocation(extensionKey, zipPath, tempPath, token); + await this.extensionsScannerService.updateMetadata(URI.file(tempPath), { ...metadata, installedTimestamp: Date.now() }); + + try { + await this.rename(extensionKey, tempPath, extensionPath, Date.now() + (2 * 60 * 1000) /* Retry for 2 minutes */); + this.logService.info('Renamed to', extensionPath); + } catch (error) { + try { + await pfs.Promises.rm(tempPath); + } catch (e) { /* ignore */ } + if (error.code === 'ENOTEMPTY') { + this.logService.info(`Rename failed because extension was installed by another source. So ignoring renaming.`, extensionKey.id); + } else { + this.logService.info(`Rename failed because of ${getErrorMessage(error)}. Deleted from extracted location`, tempPath); + throw error; + } + } + + return this.scanLocalExtension(URI.file(extensionPath), ExtensionType.User); + } + + async updateMetadata(local: ILocalExtension, metadata: Partial<Metadata>): Promise<ILocalExtension> { + await this.extensionsScannerService.updateMetadata(local.location, metadata); + return this.scanLocalExtension(local.location, local.type); + } + + getUninstalledExtensions(): Promise<IStringDictionary<boolean>> { + return this.withUninstalledExtensions(); + } + + async setUninstalled(...extensions: ILocalExtension[]): Promise<void> { + const extensionKeys: ExtensionKey[] = extensions.map(e => ExtensionKey.create(e)); + await this.withUninstalledExtensions(uninstalled => { + extensionKeys.forEach(extensionKey => uninstalled[extensionKey.toString()] = true); + }); + } + + async setInstalled(extensionKey: ExtensionKey): Promise<ILocalExtension | null> { + await this.withUninstalledExtensions(uninstalled => delete uninstalled[extensionKey.toString()]); + const userExtensions = await this.scanUserExtensions(true); + const localExtension = userExtensions.find(i => ExtensionKey.create(i).equals(extensionKey)) || null; + if (!localExtension) { + return null; + } + return this.updateMetadata(localExtension, { installedTimestamp: Date.now() }); + } + + async removeExtension(extension: ILocalExtension | IScannedExtension, type: string): Promise<void> { + this.logService.trace(`Deleting ${type} extension from disk`, extension.identifier.id, extension.location.fsPath); + await pfs.Promises.rm(extension.location.fsPath); + this.logService.info('Deleted from disk', extension.identifier.id, extension.location.fsPath); + } + + async removeUninstalledExtension(extension: ILocalExtension | IScannedExtension): Promise<void> { + await this.removeExtension(extension, 'uninstalled'); + await this.withUninstalledExtensions(uninstalled => delete uninstalled[ExtensionKey.create(extension).toString()]); + } + + private async withUninstalledExtensions(updateFn?: (uninstalled: IStringDictionary<boolean>) => void): Promise<IStringDictionary<boolean>> { + return this.uninstalledFileLimiter.queue(async () => { + let raw: string | undefined; + try { + raw = await pfs.Promises.readFile(this.uninstalledPath, 'utf8'); + } catch (err) { + if (err.code !== 'ENOENT') { + throw err; + } + } + + let uninstalled = {}; + if (raw) { + try { + uninstalled = JSON.parse(raw); + } catch (e) { /* ignore */ } + } + + if (updateFn) { + updateFn(uninstalled); + if (Object.keys(uninstalled).length) { + await pfs.Promises.writeFile(this.uninstalledPath, JSON.stringify(uninstalled)); + } else { + await pfs.Promises.rm(this.uninstalledPath); + } + } + + return uninstalled; + }); + } + + private async extractAtLocation(identifier: IExtensionIdentifier, zipPath: string, location: string, token: CancellationToken): Promise<void> { + this.logService.trace(`Started extracting the extension from ${zipPath} to ${location}`); + + // Clean the location + try { + await pfs.Promises.rm(location); + } catch (e) { + throw new ExtensionManagementError(this.joinErrors(e).message, ExtensionManagementErrorCode.Delete); + } + + try { + await extract(zipPath, location, { sourcePath: 'extension', overwrite: true }, token); + this.logService.info(`Extracted extension to ${location}:`, identifier.id); + } catch (e) { + try { await pfs.Promises.rm(location); } catch (e) { /* Ignore */ } + let errorCode = ExtensionManagementErrorCode.Extract; + if (e instanceof ExtractError) { + if (e.type === 'CorruptZip') { + errorCode = ExtensionManagementErrorCode.CorruptZip; + } else if (e.type === 'Incomplete') { + errorCode = ExtensionManagementErrorCode.IncompleteZip; + } + } + throw new ExtensionManagementError(e.message, errorCode); + } + } + + private async rename(identifier: IExtensionIdentifier, extractPath: string, renamePath: string, retryUntil: number): Promise<void> { + try { + await pfs.Promises.rename(extractPath, renamePath); + } catch (error) { + if (isWindows && error && error.code === 'EPERM' && Date.now() < retryUntil) { + this.logService.info(`Failed renaming ${extractPath} to ${renamePath} with 'EPERM' error. Trying again...`, identifier.id); + return this.rename(identifier, extractPath, renamePath, retryUntil); + } + throw new ExtensionManagementError(error.message || nls.localize('renameError', "Unknown error while renaming {0} to {1}", extractPath, renamePath), error.code || ExtensionManagementErrorCode.Rename); + } + } + + private async scanLocalExtension(location: URI, type: ExtensionType): Promise<ILocalExtension> { + const scannedExtension = await this.extensionsScannerService.scanExistingExtension(location, type, { includeInvalid: true }); + if (scannedExtension) { + return this.toLocalExtension(scannedExtension); + } + throw new Error(nls.localize('cannot read', "Cannot read the extension from {0}", location.path)); + } + + private async toLocalExtension(extension: IScannedExtension): Promise<ILocalExtension> { + const stat = await this.fileService.resolve(extension.location); + let readmeUrl: URI | undefined; + let changelogUrl: URI | undefined; + if (stat.children) { + readmeUrl = stat.children.find(({ name }) => /^readme(\.txt|\.md|)$/i.test(name))?.resource; + changelogUrl = stat.children.find(({ name }) => /^changelog(\.txt|\.md|)$/i.test(name))?.resource; + } + return { + identifier: extension.identifier, + type: extension.type, + isBuiltin: extension.isBuiltin || !!extension.metadata?.isBuiltin, + location: extension.location, + manifest: extension.manifest, + targetPlatform: extension.targetPlatform, + validations: extension.validations, + isValid: extension.isValid, + readmeUrl, + changelogUrl, + publisherDisplayName: extension.metadata?.publisherDisplayName || null, + publisherId: extension.metadata?.publisherId || null, + isMachineScoped: !!extension.metadata?.isMachineScoped, + isPreReleaseVersion: !!extension.metadata?.isPreReleaseVersion, + preRelease: !!extension.metadata?.preRelease, + installedTimestamp: extension.metadata?.installedTimestamp, + updated: !!extension.metadata?.updated, + }; + } + private async removeUninstalledExtensions(): Promise<void> { + const uninstalled = await this.getUninstalledExtensions(); + const extensions = await this.extensionsScannerService.scanUserExtensions({ includeAllVersions: true, includeUninstalled: true, includeInvalid: true }); // All user extensions + const installed: Set<string> = new Set<string>(); + for (const e of extensions) { + if (!uninstalled[ExtensionKey.create(e).toString()]) { + installed.add(e.identifier.id.toLowerCase()); + } + } + const byExtension = groupByExtension(extensions, e => e.identifier); + await Promises.settled(byExtension.map(async e => { + const latest = e.sort((a, b) => semver.rcompare(a.manifest.version, b.manifest.version))[0]; + if (!installed.has(latest.identifier.id.toLowerCase())) { + await this.beforeRemovingExtension(await this.toLocalExtension(latest)); + } + })); + const toRemove = extensions.filter(e => uninstalled[ExtensionKey.create(e).toString()]); + await Promises.settled(toRemove.map(e => this.removeUninstalledExtension(e))); + } + + private async removeOutdatedExtensions(): Promise<void> { + const extensions = await this.extensionsScannerService.scanUserExtensions({ includeAllVersions: true, includeUninstalled: true, includeInvalid: true }); // All user extensions + const toRemove: IScannedExtension[] = []; + + // Outdated extensions + const targetPlatform = await this.extensionsScannerService.getTargetPlatform(); + const byExtension = groupByExtension(extensions, e => e.identifier); + toRemove.push(...byExtension.map(p => p.sort((a, b) => { + const vcompare = semver.rcompare(a.manifest.version, b.manifest.version); + if (vcompare !== 0) { + return vcompare; + } + if (a.targetPlatform === targetPlatform) { + return -1; + } + return 1; + }).slice(1)).flat()); + + await Promises.settled(toRemove.map(extension => this.removeExtension(extension, 'outdated'))); + } + + private joinErrors(errorOrErrors: (Error | string) | (Array<Error | string>)): Error { + const errors = Array.isArray(errorOrErrors) ? errorOrErrors : [errorOrErrors]; + if (errors.length === 1) { + return errors[0] instanceof Error ? <Error>errors[0] : new Error(<string>errors[0]); + } + return errors.reduce<Error>((previousValue: Error, currentValue: Error | string) => { + return new Error(`${previousValue.message}${previousValue.message ? ',' : ''}${currentValue instanceof Error ? currentValue.message : currentValue}`); + }, new Error('')); + } + +} + abstract class AbstractInstallExtensionTask extends AbstractExtensionTask<ILocalExtension> implements IInstallExtensionTask { protected _operation = InstallOperation.Install; @@ -206,7 +475,7 @@ abstract class AbstractInstallExtensionTask extends AbstractExtensionTask<ILocal try { const local = await this.unsetUninstalledAndGetLocal(installableExtension.key); if (local) { - return installableExtension.metadata ? this.extensionsScanner.saveMetadataForLocalExtension(local, { ...((<ILocalExtensionManifest>local.manifest).__metadata || {}), ...installableExtension.metadata }) : local; + return installableExtension.metadata ? this.extensionsScanner.updateMetadata(local, installableExtension.metadata) : local; } } catch (e) { if (isMacintosh) { diff --git a/src/vs/platform/extensionManagement/node/extensionsScanner.ts b/src/vs/platform/extensionManagement/node/extensionsScanner.ts deleted file mode 100644 index 6945e43052e..00000000000 --- a/src/vs/platform/extensionManagement/node/extensionsScanner.ts +++ /dev/null @@ -1,448 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { flatten } from 'vs/base/common/arrays'; -import { Limiter, Promises, Queue } from 'vs/base/common/async'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { IStringDictionary } from 'vs/base/common/collections'; -import { getErrorMessage } from 'vs/base/common/errors'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { FileAccess } from 'vs/base/common/network'; -import * as path from 'vs/base/common/path'; -import { isWindows } from 'vs/base/common/platform'; -import { basename, isEqualOrParent, joinPath } from 'vs/base/common/resources'; -import * as semver from 'vs/base/common/semver/semver'; -import { URI } from 'vs/base/common/uri'; -import { generateUuid } from 'vs/base/common/uuid'; -import * as pfs from 'vs/base/node/pfs'; -import { extract, ExtractError } from 'vs/base/node/zip'; -import { localize } from 'vs/nls'; -import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; -import { ExtensionManagementError, ExtensionManagementErrorCode, Metadata, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { areSameExtensions, ExtensionKey, getGalleryExtensionId, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { localizeManifest } from 'vs/platform/extensionManagement/common/extensionNls'; -import { ExtensionType, IExtensionIdentifier, ExtensionIdentifier, IExtensionManifest, TargetPlatform, UNDEFINED_PUBLISHER } from 'vs/platform/extensions/common/extensions'; -import { IFileService } from 'vs/platform/files/common/files'; -import { ILogService } from 'vs/platform/log/common/log'; -import { IProductService } from 'vs/platform/product/common/productService'; - -export type ILocalExtensionManifest = IExtensionManifest & { __metadata?: Metadata }; -type IRelaxedLocalExtension = ILocalExtension & { type: ExtensionType; isBuiltin: boolean; targetPlatform: TargetPlatform }; - -export class ExtensionsScanner extends Disposable { - - private readonly systemExtensionsLocation: URI; - private readonly userExtensionsLocation: URI; - private readonly uninstalledPath: string; - private readonly uninstalledFileLimiter: Queue<any>; - - constructor( - private readonly beforeRemovingExtension: (e: ILocalExtension) => Promise<void>, - private readonly targetPlatform: Promise<TargetPlatform>, - @IFileService private readonly fileService: IFileService, - @ILogService private readonly logService: ILogService, - @INativeEnvironmentService private readonly environmentService: INativeEnvironmentService, - @IProductService private readonly productService: IProductService, - ) { - super(); - this.systemExtensionsLocation = URI.file(environmentService.builtinExtensionsPath); - this.userExtensionsLocation = URI.file(environmentService.extensionsPath); - this.uninstalledPath = joinPath(this.userExtensionsLocation, '.obsolete').fsPath; - this.uninstalledFileLimiter = new Queue(); - } - - async cleanUp(): Promise<void> { - await this.removeUninstalledExtensions(); - await this.removeOutdatedExtensions(); - } - - async scanExtensions(type: ExtensionType | null): Promise<ILocalExtension[]> { - const promises: Promise<ILocalExtension[]>[] = []; - - if (type === null || type === ExtensionType.System) { - promises.push(this.scanDefaultSystemExtensions()); - promises.push(this.environmentService.isBuilt ? Promise.resolve([]) : this.scanDevSystemExtensions()); - } else { - promises.push(Promise.resolve([])); - promises.push(Promise.resolve([])); - } - promises.push(this.scanUserExtensions(false)); - - try { - const [defaultSystemExtensions, devSystemExtensions, userExtensions] = await Promise.all(promises); - const result = this.dedupExtensions([...defaultSystemExtensions, ...devSystemExtensions, ...userExtensions], await this.targetPlatform); - return type !== null ? result.filter(r => r.type === type) : result; - } catch (error) { - throw this.joinErrors(error); - } - } - - async scanUserExtensions(excludeOutdated: boolean): Promise<ILocalExtension[]> { - this.logService.trace('Started scanning user extensions'); - let [uninstalled, extensions] = await Promise.all([this.getUninstalledExtensions(), this.scanFromUserExtensionsLocation()]); - extensions = extensions.filter(e => !uninstalled[ExtensionKey.create(e).toString()]); - extensions = excludeOutdated ? this.dedupExtensions(extensions, await this.targetPlatform) : extensions; - this.logService.trace('Scanned user extensions:', extensions.length); - return extensions; - } - - async extractUserExtension(extensionKey: ExtensionKey, zipPath: string, metadata: Metadata | undefined, token: CancellationToken): Promise<ILocalExtension> { - const folderName = extensionKey.toString(); - const tempPath = path.join(this.userExtensionsLocation.fsPath, `.${generateUuid()}`); - const extensionPath = path.join(this.userExtensionsLocation.fsPath, folderName); - - try { - await pfs.Promises.rm(extensionPath); - } catch (error) { - throw new ExtensionManagementError(localize('errorDeleting', "Unable to delete the existing folder '{0}' while installing the extension '{1}'. Please delete the folder manually and try again", extensionPath, extensionKey.id), ExtensionManagementErrorCode.Delete); - } - - await this.extractAtLocation(extensionKey, zipPath, tempPath, token); - let local = await this.scanExtension(URI.file(tempPath), ExtensionType.User); - if (!local) { - throw new Error(localize('cannot read', "Cannot read the extension from {0}", tempPath)); - } - await this.storeMetadata(local, { ...metadata, installedTimestamp: Date.now() }); - - try { - await this.rename(extensionKey, tempPath, extensionPath, Date.now() + (2 * 60 * 1000) /* Retry for 2 minutes */); - this.logService.info('Renamed to', extensionPath); - } catch (error) { - try { - await pfs.Promises.rm(tempPath); - } catch (e) { /* ignore */ } - if (error.code === 'ENOTEMPTY') { - this.logService.info(`Rename failed because extension was installed by another source. So ignoring renaming.`, extensionKey.id); - } else { - this.logService.info(`Rename failed because of ${getErrorMessage(error)}. Deleted from extracted location`, tempPath); - throw error; - } - } - - try { - local = await this.scanExtension(URI.file(extensionPath), ExtensionType.User); - } catch (e) { /*ignore */ } - - if (local) { - return local; - } - throw new Error(localize('cannot read', "Cannot read the extension from {0}", this.userExtensionsLocation.fsPath)); - } - - async saveMetadataForLocalExtension(local: ILocalExtension, metadata: Metadata): Promise<ILocalExtension> { - this.setMetadata(local, metadata); - await this.storeMetadata(local, { ...metadata, installedTimestamp: local.installedTimestamp }); - return local; - } - - getUninstalledExtensions(): Promise<IStringDictionary<boolean>> { - return this.withUninstalledExtensions(); - } - - async setUninstalled(...extensions: ILocalExtension[]): Promise<void> { - const extensionKeys: ExtensionKey[] = extensions.map(e => ExtensionKey.create(e)); - await this.withUninstalledExtensions(uninstalled => { - extensionKeys.forEach(extensionKey => uninstalled[extensionKey.toString()] = true); - }); - } - - async setInstalled(extensionKey: ExtensionKey): Promise<ILocalExtension | null> { - await this.withUninstalledExtensions(uninstalled => delete uninstalled[extensionKey.toString()]); - const userExtensions = await this.scanUserExtensions(true); - const localExtension = userExtensions.find(i => ExtensionKey.create(i).equals(extensionKey)) || null; - if (!localExtension) { - return null; - } - await this.storeMetadata(localExtension, { installedTimestamp: Date.now() }); - return this.scanExtension(localExtension.location, localExtension.type); - } - - async removeExtension(extension: ILocalExtension, type: string): Promise<void> { - this.logService.trace(`Deleting ${type} extension from disk`, extension.identifier.id, extension.location.fsPath); - await pfs.Promises.rm(extension.location.fsPath); - this.logService.info('Deleted from disk', extension.identifier.id, extension.location.fsPath); - } - - async removeUninstalledExtension(extension: ILocalExtension): Promise<void> { - await this.removeExtension(extension, 'uninstalled'); - await this.withUninstalledExtensions(uninstalled => delete uninstalled[ExtensionKey.create(extension).toString()]); - } - - private async storeMetadata(local: ILocalExtension, metaData: Metadata): Promise<ILocalExtension> { - // unset if false - metaData.isMachineScoped = metaData.isMachineScoped || undefined; - metaData.isBuiltin = metaData.isBuiltin || undefined; - metaData.installedTimestamp = metaData.installedTimestamp || undefined; - const manifestPath = path.join(local.location.fsPath, 'package.json'); - const raw = await pfs.Promises.readFile(manifestPath, 'utf8'); - const { manifest } = await this.parseManifest(raw); - (manifest as ILocalExtensionManifest).__metadata = metaData; - await pfs.Promises.writeFile(manifestPath, JSON.stringify(manifest, null, '\t')); - return local; - } - - private async withUninstalledExtensions(updateFn?: (uninstalled: IStringDictionary<boolean>) => void): Promise<IStringDictionary<boolean>> { - return this.uninstalledFileLimiter.queue(async () => { - let raw: string | undefined; - try { - raw = await pfs.Promises.readFile(this.uninstalledPath, 'utf8'); - } catch (err) { - if (err.code !== 'ENOENT') { - throw err; - } - } - - let uninstalled = {}; - if (raw) { - try { - uninstalled = JSON.parse(raw); - } catch (e) { /* ignore */ } - } - - if (updateFn) { - updateFn(uninstalled); - if (Object.keys(uninstalled).length) { - await pfs.Promises.writeFile(this.uninstalledPath, JSON.stringify(uninstalled)); - } else { - await pfs.Promises.rm(this.uninstalledPath); - } - } - - return uninstalled; - }); - } - - private async extractAtLocation(identifier: IExtensionIdentifier, zipPath: string, location: string, token: CancellationToken): Promise<void> { - this.logService.trace(`Started extracting the extension from ${zipPath} to ${location}`); - - // Clean the location - try { - await pfs.Promises.rm(location); - } catch (e) { - throw new ExtensionManagementError(this.joinErrors(e).message, ExtensionManagementErrorCode.Delete); - } - - try { - await extract(zipPath, location, { sourcePath: 'extension', overwrite: true }, token); - this.logService.info(`Extracted extension to ${location}:`, identifier.id); - } catch (e) { - try { await pfs.Promises.rm(location); } catch (e) { /* Ignore */ } - let errorCode = ExtensionManagementErrorCode.Extract; - if (e instanceof ExtractError) { - if (e.type === 'CorruptZip') { - errorCode = ExtensionManagementErrorCode.CorruptZip; - } else if (e.type === 'Incomplete') { - errorCode = ExtensionManagementErrorCode.IncompleteZip; - } - } - throw new ExtensionManagementError(e.message, errorCode); - } - } - - private async rename(identifier: IExtensionIdentifier, extractPath: string, renamePath: string, retryUntil: number): Promise<void> { - try { - await pfs.Promises.rename(extractPath, renamePath); - } catch (error) { - if (isWindows && error && error.code === 'EPERM' && Date.now() < retryUntil) { - this.logService.info(`Failed renaming ${extractPath} to ${renamePath} with 'EPERM' error. Trying again...`, identifier.id); - return this.rename(identifier, extractPath, renamePath, retryUntil); - } - throw new ExtensionManagementError(error.message || localize('renameError', "Unknown error while renaming {0} to {1}", extractPath, renamePath), error.code || ExtensionManagementErrorCode.Rename); - } - } - - private async scanExtensionsInLocation(location: URI, preferredType: ExtensionType): Promise<ILocalExtension[]> { - const limiter = new Limiter<any>(10); - const stat = await this.fileService.resolve(location); - if (stat.children) { - const extensions = await Promise.all<ILocalExtension>(stat.children.filter(c => c.isDirectory) - .map(c => limiter.queue(async () => { - if (isEqualOrParent(c.resource, this.userExtensionsLocation) && basename(c.resource).indexOf('.') === 0) { // Do not consider user extension folder starting with `.` - return null; - } - return this.scanExtension(c.resource, preferredType); - }))); - return extensions.filter(e => e && e.identifier); - } - return []; - } - - private async scanExtension(extensionLocation: URI, preferredType: ExtensionType): Promise<ILocalExtension | null> { - try { - const stat = await this.fileService.resolve(extensionLocation); - if (stat.children) { - const { manifest, metadata } = await this.readManifest(extensionLocation.fsPath); - const readmeUrl = stat.children.find(({ name }) => /^readme(\.txt|\.md|)$/i.test(name))?.resource; - const changelogUrl = stat.children.find(({ name }) => /^changelog(\.txt|\.md|)$/i.test(name))?.resource; - const identifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name) }; - const type = metadata?.isSystem ? ExtensionType.System : preferredType; - const local = <ILocalExtension>{ type, identifier, manifest, location: extensionLocation, readmeUrl, changelogUrl, publisherDisplayName: null, publisherId: null, isMachineScoped: false, isBuiltin: type === ExtensionType.System }; - this.setMetadata(local, metadata); - return local; - } - } catch (e) { - if (preferredType !== ExtensionType.System) { - this.logService.trace(e); - } - } - return null; - } - - private async scanDefaultSystemExtensions(): Promise<ILocalExtension[]> { - this.logService.trace('Started scanning system extensions'); - const result = await this.scanExtensionsInLocation(this.systemExtensionsLocation, ExtensionType.System); - this.logService.trace('Scanned system extensions:', result.length); - return result; - } - - private async scanDevSystemExtensions(): Promise<ILocalExtension[]> { - this.logService.trace('Started scanning dev system extensions'); - const devSystemExtensionsList = this.getDevSystemExtensionsList(); - if (devSystemExtensionsList.length) { - const result = await this.scanExtensionsInLocation(this.devSystemExtensionsLocation, ExtensionType.System); - this.logService.trace('Scanned dev system extensions:', result.length); - return result.filter(r => devSystemExtensionsList.some(id => areSameExtensions(r.identifier, { id }))); - } else { - return []; - } - } - - private async scanFromUserExtensionsLocation(): Promise<ILocalExtension[]> { - return this.scanExtensionsInLocation(this.userExtensionsLocation, ExtensionType.User); - } - - private dedupExtensions(extensions: ILocalExtension[], targetPlatform: TargetPlatform): ILocalExtension[] { - const result = new Map<string, ILocalExtension>(); - for (const extension of extensions) { - const extensionKey = ExtensionIdentifier.toKey(extension.identifier.id); - const existing = result.get(extensionKey); - if (existing) { - if (semver.gt(existing.manifest.version, extension.manifest.version)) { - this.logService.debug(`Skipping extension ${extension.location.fsPath} with lower version ${extension.manifest.version}.`); - continue; - } - if (semver.eq(existing.manifest.version, extension.manifest.version) && existing.targetPlatform === targetPlatform) { - this.logService.debug(`Skipping extension ${extension.location.fsPath} from different target platform ${extension.targetPlatform}`); - continue; - } - if (existing.type === ExtensionType.System) { - this.logService.debug(`Overwriting system extension ${existing.location.fsPath} with ${extension.location.fsPath}.`); - } else { - this.logService.warn(`Overwriting user extension ${existing.location.fsPath} with ${extension.location.fsPath}.`); - } - } - result.set(extensionKey, extension); - } - return [...result.values()]; - } - - private setMetadata(local: IRelaxedLocalExtension, metadata: Metadata | null): void { - local.publisherDisplayName = metadata?.publisherDisplayName || null; - local.publisherId = metadata?.publisherId || null; - local.identifier.uuid = metadata?.id; - local.isMachineScoped = !!metadata?.isMachineScoped; - local.isPreReleaseVersion = !!metadata?.isPreReleaseVersion; - local.preRelease = !!metadata?.preRelease; - local.isBuiltin = local.type === ExtensionType.System || !!metadata?.isBuiltin; - local.installedTimestamp = metadata?.installedTimestamp; - local.targetPlatform = metadata?.targetPlatform ?? TargetPlatform.UNDEFINED; - local.updated = !!metadata?.updated; - } - - private async removeUninstalledExtensions(): Promise<void> { - const uninstalled = await this.getUninstalledExtensions(); - const extensions = await this.scanFromUserExtensionsLocation(); // All user extensions - const installed: Set<string> = new Set<string>(); - for (const e of extensions) { - if (!uninstalled[ExtensionKey.create(e).toString()]) { - installed.add(e.identifier.id.toLowerCase()); - } - } - const byExtension: ILocalExtension[][] = groupByExtension(extensions, e => e.identifier); - await Promises.settled(byExtension.map(async e => { - const latest = e.sort((a, b) => semver.rcompare(a.manifest.version, b.manifest.version))[0]; - if (!installed.has(latest.identifier.id.toLowerCase())) { - await this.beforeRemovingExtension(latest); - } - })); - const toRemove: ILocalExtension[] = extensions.filter(e => uninstalled[ExtensionKey.create(e).toString()]); - await Promises.settled(toRemove.map(e => this.removeUninstalledExtension(e))); - } - - private async removeOutdatedExtensions(): Promise<void> { - const extensions = await this.scanFromUserExtensionsLocation(); - const toRemove: ILocalExtension[] = []; - - // Outdated extensions - const targetPlatform = await this.targetPlatform; - const byExtension: ILocalExtension[][] = groupByExtension(extensions, e => e.identifier); - toRemove.push(...flatten(byExtension.map(p => p.sort((a, b) => { - const vcompare = semver.rcompare(a.manifest.version, b.manifest.version); - if (vcompare !== 0) { - return vcompare; - } - if (a.targetPlatform === targetPlatform) { - return -1; - } - return 1; - }).slice(1)))); - - await Promises.settled(toRemove.map(extension => this.removeExtension(extension, 'outdated'))); - } - - private getDevSystemExtensionsList(): string[] { - return (this.productService.builtInExtensions || []).map(e => e.name); - } - - private joinErrors(errorOrErrors: (Error | string) | (Array<Error | string>)): Error { - const errors = Array.isArray(errorOrErrors) ? errorOrErrors : [errorOrErrors]; - if (errors.length === 1) { - return errors[0] instanceof Error ? <Error>errors[0] : new Error(<string>errors[0]); - } - return errors.reduce<Error>((previousValue: Error, currentValue: Error | string) => { - return new Error(`${previousValue.message}${previousValue.message ? ',' : ''}${currentValue instanceof Error ? currentValue.message : currentValue}`); - }, new Error('')); - } - - private _devSystemExtensionsLocation: URI | null = null; - private get devSystemExtensionsLocation(): URI { - if (!this._devSystemExtensionsLocation) { - this._devSystemExtensionsLocation = URI.file(path.normalize(path.join(FileAccess.asFileUri('', require).fsPath, '..', '.build', 'builtInExtensions'))); - } - return this._devSystemExtensionsLocation; - } - - private async readManifest(extensionPath: string): Promise<{ manifest: IExtensionManifest; metadata: Metadata | null }> { - const promises = [ - pfs.Promises.readFile(path.join(extensionPath, 'package.json'), 'utf8') - .then(raw => this.parseManifest(raw)), - pfs.Promises.readFile(path.join(extensionPath, 'package.nls.json'), 'utf8') - .then(undefined, err => err.code !== 'ENOENT' ? Promise.reject<string>(err) : '{}') - .then(raw => JSON.parse(raw)) - ]; - - const [{ manifest, metadata }, translations] = await Promise.all(promises); - return { - manifest: localizeManifest(manifest, translations), - metadata - }; - } - - private parseManifest(raw: string): Promise<{ manifest: IExtensionManifest; metadata: Metadata | null }> { - return new Promise((c, e) => { - try { - const manifest = <ILocalExtensionManifest & { publisher: string }>JSON.parse(raw); - // allow publisher to be undefined to make the initial extension authoring experience smoother - if (!manifest.publisher) { - manifest.publisher = UNDEFINED_PUBLISHER; - } - const metadata = manifest.__metadata || null; - c({ manifest, metadata }); - } catch (err) { - e(new Error(localize('invalidManifest', "Extension invalid: package.json is not a JSON file."))); - } - }); - } -} diff --git a/src/vs/platform/extensionManagement/node/extensionsScannerService.ts b/src/vs/platform/extensionManagement/node/extensionsScannerService.ts new file mode 100644 index 00000000000..500220cfb58 --- /dev/null +++ b/src/vs/platform/extensionManagement/node/extensionsScannerService.ts @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI } from 'vs/base/common/uri'; +import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IExtensionsScannerService, NativeExtensionsScannerService, } from 'vs/platform/extensionManagement/common/extensionsScannerService'; +import { IFileService } from 'vs/platform/files/common/files'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IProductService } from 'vs/platform/product/common/productService'; + +export class ExtensionsScannerService extends NativeExtensionsScannerService implements IExtensionsScannerService { + + constructor( + @IFileService fileService: IFileService, + @ILogService logService: ILogService, + @INativeEnvironmentService environmentService: INativeEnvironmentService, + @IProductService productService: IProductService, + ) { + super( + URI.file(environmentService.builtinExtensionsPath), + URI.file(environmentService.extensionsPath), + environmentService.userHome, + URI.file(environmentService.userDataPath), + fileService, logService, environmentService, productService); + } + +} diff --git a/src/vs/platform/extensionManagement/node/extensionsWatcher.ts b/src/vs/platform/extensionManagement/node/extensionsWatcher.ts index 5eda8e0d4b2..2693b4e10bd 100644 --- a/src/vs/platform/extensionManagement/node/extensionsWatcher.ts +++ b/src/vs/platform/extensionManagement/node/extensionsWatcher.ts @@ -60,7 +60,7 @@ export class ExtensionsWatcher extends Disposable { return true; } - // Ingore changes to files starting with `.` + // Ignore changes to files starting with `.` if (this.uriIdentityService.extUri.basename(change.resource).startsWith('.')) { return false; } diff --git a/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts b/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts new file mode 100644 index 00000000000..825f7ab275a --- /dev/null +++ b/src/vs/platform/extensionManagement/test/node/extensionsScannerService.test.ts @@ -0,0 +1,331 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as assert from 'assert'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { dirname, joinPath } from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; +import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; +import { AbstractExtensionsScannerService, IExtensionsScannerService, IScannedExtensionManifest, Translations } from 'vs/platform/extensionManagement/common/extensionsScannerService'; +import { ExtensionType, IExtensionManifest, MANIFEST_CACHE_FOLDER, TargetPlatform } from 'vs/platform/extensions/common/extensions'; +import { IFileService } from 'vs/platform/files/common/files'; +import { FileService } from 'vs/platform/files/common/fileService'; +import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { ILogService, NullLogService } from 'vs/platform/log/common/log'; +import { IProductService } from 'vs/platform/product/common/productService'; + +let translations: Translations = Object.create(null); +const ROOT = URI.file('/ROOT'); + +class ExtensionsScannerService extends AbstractExtensionsScannerService implements IExtensionsScannerService { + + constructor( + @IFileService fileService: IFileService, + @ILogService logService: ILogService, + @INativeEnvironmentService nativeEnvironmentService: INativeEnvironmentService, + @IProductService productService: IProductService, + ) { + super( + URI.file(nativeEnvironmentService.builtinExtensionsPath), + URI.file(nativeEnvironmentService.extensionsPath), + joinPath(nativeEnvironmentService.userHome, '.vscode-oss-dev', 'extensions', 'control.json'), + joinPath(ROOT, MANIFEST_CACHE_FOLDER), + fileService, logService, nativeEnvironmentService, productService); + } + + protected async getTranslations(language: string): Promise<Translations> { + return translations; + } + +} + +suite('NativeExtensionsScanerService Test', () => { + + const disposables = new DisposableStore(); + let instantiationService: TestInstantiationService; + + setup(async () => { + translations = {}; + instantiationService = new TestInstantiationService(); + const logService = new NullLogService(); + const fileService = disposables.add(new FileService(logService)); + const fileSystemProvider = disposables.add(new InMemoryFileSystemProvider()); + fileService.registerProvider(ROOT.scheme, fileSystemProvider); + instantiationService.stub(ILogService, logService); + instantiationService.stub(IFileService, fileService); + const systemExtensionsLocation = joinPath(ROOT, 'system'); + const userExtensionsLocation = joinPath(ROOT, 'extensions'); + instantiationService.stub(INativeEnvironmentService, { + userHome: ROOT, + builtinExtensionsPath: systemExtensionsLocation.fsPath, + extensionsPath: userExtensionsLocation.fsPath, + }); + instantiationService.stub(IProductService, { version: '1.66.0' }); + await fileService.createFolder(systemExtensionsLocation); + await fileService.createFolder(userExtensionsLocation); + }); + + teardown(() => disposables.clear()); + + test('scan system extension', async () => { + const manifest: Partial<IExtensionManifest> = anExtensionManifest({ 'name': 'name', 'publisher': 'pub' }); + const extensionLocation = await aSystemExtension(manifest); + const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService); + + const actual = await testObject.scanSystemExtensions({}); + + assert.deepStrictEqual(actual.length, 1); + assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); + assert.deepStrictEqual(actual[0].location.toString(), extensionLocation.toString()); + assert.deepStrictEqual(actual[0].isBuiltin, true); + assert.deepStrictEqual(actual[0].type, ExtensionType.System); + assert.deepStrictEqual(actual[0].isValid, true); + assert.deepStrictEqual(actual[0].validations, []); + assert.deepStrictEqual(actual[0].metadata, undefined); + assert.deepStrictEqual(actual[0].targetPlatform, TargetPlatform.UNDEFINED); + assert.deepStrictEqual(actual[0].manifest, manifest); + }); + + test('scan user extension', async () => { + const manifest: Partial<IScannedExtensionManifest> = anExtensionManifest({ 'name': 'name', 'publisher': 'pub', __metadata: { id: 'uuid' } }); + const extensionLocation = await aUserExtension(manifest); + const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService); + + const actual = await testObject.scanUserExtensions({}); + + assert.deepStrictEqual(actual.length, 1); + assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name', uuid: 'uuid' }); + assert.deepStrictEqual(actual[0].location.toString(), extensionLocation.toString()); + assert.deepStrictEqual(actual[0].isBuiltin, false); + assert.deepStrictEqual(actual[0].type, ExtensionType.User); + assert.deepStrictEqual(actual[0].isValid, true); + assert.deepStrictEqual(actual[0].validations, []); + assert.deepStrictEqual(actual[0].metadata, { id: 'uuid' }); + assert.deepStrictEqual(actual[0].targetPlatform, TargetPlatform.UNDEFINED); + delete manifest.__metadata; + assert.deepStrictEqual(actual[0].manifest, manifest); + }); + + test('scan existing extension', async () => { + const manifest: Partial<IExtensionManifest> = anExtensionManifest({ 'name': 'name', 'publisher': 'pub' }); + const extensionLocation = await aUserExtension(manifest); + const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService); + + const actual = await testObject.scanExistingExtension(extensionLocation, ExtensionType.User, {}); + + assert.notEqual(actual, null); + assert.deepStrictEqual(actual!.identifier, { id: 'pub.name' }); + assert.deepStrictEqual(actual!.location.toString(), extensionLocation.toString()); + assert.deepStrictEqual(actual!.isBuiltin, false); + assert.deepStrictEqual(actual!.type, ExtensionType.User); + assert.deepStrictEqual(actual!.isValid, true); + assert.deepStrictEqual(actual!.validations, []); + assert.deepStrictEqual(actual!.metadata, undefined); + assert.deepStrictEqual(actual!.targetPlatform, TargetPlatform.UNDEFINED); + assert.deepStrictEqual(actual!.manifest, manifest); + }); + + test('scan single extension', async () => { + const manifest: Partial<IExtensionManifest> = anExtensionManifest({ 'name': 'name', 'publisher': 'pub' }); + const extensionLocation = await aUserExtension(manifest); + const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService); + + const actual = await testObject.scanOneOrMultipleExtensions(extensionLocation, ExtensionType.User, {}); + + assert.deepStrictEqual(actual.length, 1); + assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); + assert.deepStrictEqual(actual[0].location.toString(), extensionLocation.toString()); + assert.deepStrictEqual(actual[0].isBuiltin, false); + assert.deepStrictEqual(actual[0].type, ExtensionType.User); + assert.deepStrictEqual(actual[0].isValid, true); + assert.deepStrictEqual(actual[0].validations, []); + assert.deepStrictEqual(actual[0].metadata, undefined); + assert.deepStrictEqual(actual[0].targetPlatform, TargetPlatform.UNDEFINED); + assert.deepStrictEqual(actual[0].manifest, manifest); + }); + + test('scan multiple extensions', async () => { + const extensionLocation = await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' })); + await aUserExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub' })); + const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService); + + const actual = await testObject.scanOneOrMultipleExtensions(dirname(extensionLocation), ExtensionType.User, {}); + + assert.deepStrictEqual(actual.length, 2); + assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); + assert.deepStrictEqual(actual[1].identifier, { id: 'pub.name2' }); + }); + + test('scan user extension with different versions', async () => { + await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.1' })); + await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.2' })); + const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService); + + const actual = await testObject.scanUserExtensions({}); + + assert.deepStrictEqual(actual.length, 1); + assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); + assert.deepStrictEqual(actual[0].manifest.version, '1.0.2'); + }); + + test('scan user extension include all versions', async () => { + await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.1' })); + await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.2' })); + const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService); + + const actual = await testObject.scanUserExtensions({ includeAllVersions: true }); + + assert.deepStrictEqual(actual.length, 2); + assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); + assert.deepStrictEqual(actual[0].manifest.version, '1.0.1'); + assert.deepStrictEqual(actual[1].identifier, { id: 'pub.name' }); + assert.deepStrictEqual(actual[1].manifest.version, '1.0.2'); + }); + + test('scan user extension with different versions and higher version is not compatible', async () => { + await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.1' })); + await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.2', engines: { vscode: '^1.67.0' } })); + const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService); + + const actual = await testObject.scanUserExtensions({}); + + assert.deepStrictEqual(actual.length, 1); + assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); + assert.deepStrictEqual(actual[0].manifest.version, '1.0.1'); + }); + + test('scan exclude invalid extensions', async () => { + await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' })); + await aUserExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub', engines: { vscode: '^1.67.0' } })); + const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService); + + const actual = await testObject.scanUserExtensions({}); + + assert.deepStrictEqual(actual.length, 1); + assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); + }); + + test('scan exclude uninstalled extensions', async () => { + await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' })); + await aUserExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub' })); + await instantiationService.get(IFileService).writeFile(joinPath(URI.file(instantiationService.get(INativeEnvironmentService).extensionsPath), '.obsolete'), VSBuffer.fromString(JSON.stringify({ 'pub.name2-1.0.0': true }))); + const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService); + + const actual = await testObject.scanUserExtensions({}); + + assert.deepStrictEqual(actual.length, 1); + assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); + }); + + test('scan include uninstalled extensions', async () => { + await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' })); + await aUserExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub' })); + await instantiationService.get(IFileService).writeFile(joinPath(URI.file(instantiationService.get(INativeEnvironmentService).extensionsPath), '.obsolete'), VSBuffer.fromString(JSON.stringify({ 'pub.name2-1.0.0': true }))); + const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService); + + const actual = await testObject.scanUserExtensions({ includeUninstalled: true }); + + assert.deepStrictEqual(actual.length, 2); + assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); + assert.deepStrictEqual(actual[1].identifier, { id: 'pub.name2' }); + }); + + test('scan include invalid extensions', async () => { + await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' })); + await aUserExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub', engines: { vscode: '^1.67.0' } })); + const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService); + + const actual = await testObject.scanUserExtensions({ includeInvalid: true }); + + assert.deepStrictEqual(actual.length, 2); + assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); + assert.deepStrictEqual(actual[1].identifier, { id: 'pub.name2' }); + }); + + test('scan system extensions include additional builtin extensions', async () => { + instantiationService.stub(IProductService, { + version: '1.66.0', + builtInExtensions: [ + { name: 'pub.name2', version: '', repo: '', metadata: undefined }, + { name: 'pub.name', version: '', repo: '', metadata: undefined } + ] + }); + await anExtension(anExtensionManifest({ 'name': 'name2', 'publisher': 'pub' }), joinPath(ROOT, 'additional')); + const extensionLocation = await anExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub' }), joinPath(ROOT, 'additional')); + await aSystemExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', version: '1.0.1' })); + await instantiationService.get(IFileService).writeFile(joinPath(instantiationService.get(INativeEnvironmentService).userHome, '.vscode-oss-dev', 'extensions', 'control.json'), VSBuffer.fromString(JSON.stringify({ 'pub.name2': 'disabled', 'pub.name': extensionLocation.fsPath }))); + const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService); + + const actual = await testObject.scanSystemExtensions({ checkControlFile: true }); + + assert.deepStrictEqual(actual.length, 1); + assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); + assert.deepStrictEqual(actual[0].manifest.version, '1.0.0'); + }); + + test('scan extension with default nls replacements', async () => { + const extensionLocation = await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', displayName: '%displayName%' })); + await instantiationService.get(IFileService).writeFile(joinPath(extensionLocation, 'package.nls.json'), VSBuffer.fromString(JSON.stringify({ displayName: 'Hello World' }))); + const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService); + + const actual = await testObject.scanUserExtensions({}); + + assert.deepStrictEqual(actual.length, 1); + assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); + assert.deepStrictEqual(actual[0].manifest.displayName, 'Hello World'); + }); + + test('scan extension with en nls replacements', async () => { + const extensionLocation = await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', displayName: '%displayName%' })); + await instantiationService.get(IFileService).writeFile(joinPath(extensionLocation, 'package.nls.json'), VSBuffer.fromString(JSON.stringify({ displayName: 'Hello World' }))); + const nlsLocation = joinPath(extensionLocation, 'package.en.json'); + await instantiationService.get(IFileService).writeFile(nlsLocation, VSBuffer.fromString(JSON.stringify({ contents: { package: { displayName: 'Hello World EN' } } }))); + const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService); + + translations = { 'pub.name': nlsLocation.fsPath }; + const actual = await testObject.scanUserExtensions({ language: 'en' }); + + assert.deepStrictEqual(actual.length, 1); + assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); + assert.deepStrictEqual(actual[0].manifest.displayName, 'Hello World EN'); + }); + + test('scan extension falls back to default nls replacements', async () => { + const extensionLocation = await aUserExtension(anExtensionManifest({ 'name': 'name', 'publisher': 'pub', displayName: '%displayName%' })); + await instantiationService.get(IFileService).writeFile(joinPath(extensionLocation, 'package.nls.json'), VSBuffer.fromString(JSON.stringify({ displayName: 'Hello World' }))); + const nlsLocation = joinPath(extensionLocation, 'package.en.json'); + await instantiationService.get(IFileService).writeFile(nlsLocation, VSBuffer.fromString(JSON.stringify({ contents: { package: { displayName: 'Hello World EN' } } }))); + const testObject: IExtensionsScannerService = instantiationService.createInstance(ExtensionsScannerService); + + translations = { 'pub.name2': nlsLocation.fsPath }; + const actual = await testObject.scanUserExtensions({ language: 'en' }); + + assert.deepStrictEqual(actual.length, 1); + assert.deepStrictEqual(actual[0].identifier, { id: 'pub.name' }); + assert.deepStrictEqual(actual[0].manifest.displayName, 'Hello World'); + }); + + async function aUserExtension(manifest: Partial<IScannedExtensionManifest>): Promise<URI> { + const environmentService = instantiationService.get(INativeEnvironmentService); + return anExtension(manifest, URI.file(environmentService.extensionsPath)); + } + + async function aSystemExtension(manifest: Partial<IScannedExtensionManifest>): Promise<URI> { + const environmentService = instantiationService.get(INativeEnvironmentService); + return anExtension(manifest, URI.file(environmentService.builtinExtensionsPath)); + } + + async function anExtension(manifest: Partial<IScannedExtensionManifest>, root: URI): Promise<URI> { + const fileService = instantiationService.get(IFileService); + const extensionLocation = joinPath(root, `${manifest.publisher}.${manifest.name}-${manifest.version}-${manifest.__metadata?.targetPlatform ?? TargetPlatform.UNDEFINED}`); + await fileService.writeFile(joinPath(extensionLocation, 'package.json'), VSBuffer.fromString(JSON.stringify(manifest))); + return extensionLocation; + } + + function anExtensionManifest(manifest: Partial<IScannedExtensionManifest>): Partial<IExtensionManifest> { + return { engines: { vscode: '^1.66.0' }, version: '1.0.0', main: 'main.js', activationEvents: ['*'], ...manifest }; + } +}); diff --git a/src/vs/platform/extensions/common/extensionValidator.ts b/src/vs/platform/extensions/common/extensionValidator.ts index dfeea91105f..c36ca616ae2 100644 --- a/src/vs/platform/extensions/common/extensionValidator.ts +++ b/src/vs/platform/extensions/common/extensionValidator.ts @@ -3,7 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { isEqualOrParent, joinPath } from 'vs/base/common/resources'; +import Severity from 'vs/base/common/severity'; +import { URI } from 'vs/base/common/uri'; import * as nls from 'vs/nls'; +import * as semver from 'vs/base/common/semver/semver'; import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'; export interface IParsedVersion { @@ -235,6 +239,98 @@ export function isValidVersion(_inputVersion: string | INormalizedVersion, _inpu type ProductDate = string | Date | undefined; +export function validateExtensionManifest(productVersion: string, productDate: ProductDate, extensionLocation: URI, extensionManifest: IExtensionManifest, extensionIsBuiltin: boolean): readonly [Severity, string][] { + const validations: [Severity, string][] = []; + if (typeof extensionManifest.publisher !== 'undefined' && typeof extensionManifest.publisher !== 'string') { + validations.push([Severity.Error, nls.localize('extensionDescription.publisher', "property publisher must be of type `string`.")]); + return validations; + } + if (typeof extensionManifest.name !== 'string') { + validations.push([Severity.Error, nls.localize('extensionDescription.name', "property `{0}` is mandatory and must be of type `string`", 'name')]); + return validations; + } + if (typeof extensionManifest.version !== 'string') { + validations.push([Severity.Error, nls.localize('extensionDescription.version', "property `{0}` is mandatory and must be of type `string`", 'version')]); + return validations; + } + if (!extensionManifest.engines) { + validations.push([Severity.Error, nls.localize('extensionDescription.engines', "property `{0}` is mandatory and must be of type `object`", 'engines')]); + return validations; + } + if (typeof extensionManifest.engines.vscode !== 'string') { + validations.push([Severity.Error, nls.localize('extensionDescription.engines.vscode', "property `{0}` is mandatory and must be of type `string`", 'engines.vscode')]); + return validations; + } + if (typeof extensionManifest.extensionDependencies !== 'undefined') { + if (!isStringArray(extensionManifest.extensionDependencies)) { + validations.push([Severity.Error, nls.localize('extensionDescription.extensionDependencies', "property `{0}` can be omitted or must be of type `string[]`", 'extensionDependencies')]); + return validations; + } + } + if (typeof extensionManifest.activationEvents !== 'undefined') { + if (!isStringArray(extensionManifest.activationEvents)) { + validations.push([Severity.Error, nls.localize('extensionDescription.activationEvents1', "property `{0}` can be omitted or must be of type `string[]`", 'activationEvents')]); + return validations; + } + if (typeof extensionManifest.main === 'undefined' && typeof extensionManifest.browser === 'undefined') { + validations.push([Severity.Error, nls.localize('extensionDescription.activationEvents2', "properties `{0}` and `{1}` must both be specified or must both be omitted", 'activationEvents', 'main')]); + return validations; + } + } + if (typeof extensionManifest.extensionKind !== 'undefined') { + if (typeof extensionManifest.main === 'undefined') { + validations.push([Severity.Warning, nls.localize('extensionDescription.extensionKind', "property `{0}` can be defined only if property `main` is also defined.", 'extensionKind')]); + // not a failure case + } + } + if (typeof extensionManifest.main !== 'undefined') { + if (typeof extensionManifest.main !== 'string') { + validations.push([Severity.Error, nls.localize('extensionDescription.main1', "property `{0}` can be omitted or must be of type `string`", 'main')]); + return validations; + } else { + const mainLocation = joinPath(extensionLocation, extensionManifest.main); + if (!isEqualOrParent(mainLocation, extensionLocation)) { + validations.push([Severity.Warning, nls.localize('extensionDescription.main2', "Expected `main` ({0}) to be included inside extension's folder ({1}). This might make the extension non-portable.", mainLocation.path, extensionLocation.path)]); + // not a failure case + } + } + if (typeof extensionManifest.activationEvents === 'undefined') { + validations.push([Severity.Error, nls.localize('extensionDescription.main3', "properties `{0}` and `{1}` must both be specified or must both be omitted", 'activationEvents', 'main')]); + return validations; + } + } + if (typeof extensionManifest.browser !== 'undefined') { + if (typeof extensionManifest.browser !== 'string') { + validations.push([Severity.Error, nls.localize('extensionDescription.browser1', "property `{0}` can be omitted or must be of type `string`", 'browser')]); + return validations; + } else { + const browserLocation = joinPath(extensionLocation, extensionManifest.browser); + if (!isEqualOrParent(browserLocation, extensionLocation)) { + validations.push([Severity.Warning, nls.localize('extensionDescription.browser2', "Expected `browser` ({0}) to be included inside extension's folder ({1}). This might make the extension non-portable.", browserLocation.path, extensionLocation.path)]); + // not a failure case + } + } + if (typeof extensionManifest.activationEvents === 'undefined') { + validations.push([Severity.Error, nls.localize('extensionDescription.browser3', "properties `{0}` and `{1}` must both be specified or must both be omitted", 'activationEvents', 'browser')]); + return validations; + } + } + + if (!semver.valid(extensionManifest.version)) { + validations.push([Severity.Error, nls.localize('notSemver', "Extension version is not semver compatible.")]); + return validations; + } + + const notices: string[] = []; + const isValid = isValidExtensionVersion(productVersion, productDate, extensionManifest, extensionIsBuiltin, notices); + if (!isValid) { + for (const notice of notices) { + validations.push([Severity.Error, notice]); + } + } + return validations; +} + export function isValidExtensionVersion(productVersion: string, productDate: ProductDate, extensionManifest: IExtensionManifest, extensionIsBuiltin: boolean, notices: string[]): boolean { if (extensionIsBuiltin || (typeof extensionManifest.main === 'undefined' && typeof extensionManifest.browser === 'undefined')) { @@ -282,3 +378,15 @@ function isVersionValid(currentVersion: string, date: ProductDate, requestedVers return true; } + +function isStringArray(arr: string[]): boolean { + if (!Array.isArray(arr)) { + return false; + } + for (let i = 0, len = arr.length; i < len; i++) { + if (typeof arr[i] !== 'string') { + return false; + } + } + return true; +} diff --git a/src/vs/platform/extensions/common/extensions.ts b/src/vs/platform/extensions/common/extensions.ts index 0bfebc689dd..5ee57cacb6b 100644 --- a/src/vs/platform/extensions/common/extensions.ts +++ b/src/vs/platform/extensions/common/extensions.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import Severity from 'vs/base/common/severity'; import * as strings from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { ExtensionKind } from 'vs/platform/environment/common/environment'; @@ -320,6 +321,8 @@ export interface IExtension { readonly targetPlatform: TargetPlatform; readonly readmeUrl?: URI; readonly changelogUrl?: URI; + readonly isValid: boolean; + readonly validations: readonly [Severity, string][]; } /** diff --git a/src/vs/platform/files/common/diskFileSystemProvider.ts b/src/vs/platform/files/common/diskFileSystemProvider.ts index 431a28d563d..374f7c9bcf5 100644 --- a/src/vs/platform/files/common/diskFileSystemProvider.ts +++ b/src/vs/platform/files/common/diskFileSystemProvider.ts @@ -10,7 +10,7 @@ import { Emitter } from 'vs/base/common/event'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { normalize } from 'vs/base/common/path'; import { URI } from 'vs/base/common/uri'; -import { IFileChange, IWatchOptions } from 'vs/platform/files/common/files'; +import { IFileChange, IFileSystemProvider, IWatchOptions } from 'vs/platform/files/common/files'; import { AbstractNonRecursiveWatcherClient, AbstractUniversalWatcherClient, IDiskFileChange, ILogMessage, INonRecursiveWatchRequest, IRecursiveWatcherOptions, isRecursiveWatchRequest, IUniversalWatchRequest, toFileChanges } from 'vs/platform/files/common/watcher'; import { ILogService, LogLevel } from 'vs/platform/log/common/log'; @@ -36,7 +36,10 @@ export interface IDiskFileSystemProviderOptions { }; } -export abstract class AbstractDiskFileSystemProvider extends Disposable { +export abstract class AbstractDiskFileSystemProvider extends Disposable implements + Pick<IFileSystemProvider, 'watch'>, + Pick<IFileSystemProvider, 'onDidChangeFile'>, + Pick<IFileSystemProvider, 'onDidWatchError'> { constructor( protected readonly logService: ILogService, diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index 1b75291e899..47e7c042f8f 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -7,7 +7,7 @@ import { VSBuffer, VSBufferReadable, VSBufferReadableStream } from 'vs/base/comm import { CancellationToken } from 'vs/base/common/cancellation'; import { ErrorNoTelemetry } from 'vs/base/common/errors'; import { Event } from 'vs/base/common/event'; -import { IExpression } from 'vs/base/common/glob'; +import { IExpression, IRelativePattern } from 'vs/base/common/glob'; import { IDisposable } from 'vs/base/common/lifecycle'; import { TernarySearchTree } from 'vs/base/common/map'; import { sep } from 'vs/base/common/path'; @@ -238,7 +238,7 @@ export interface IFileService { * Note: recursive file watching is not supported from this method. Only events from files * that are direct children of the provided resource will be reported. */ - watch(resource: URI): IDisposable; + watch(resource: URI, options?: IWatchOptions): IDisposable; /** * Frees up any resources occupied by this service. @@ -437,7 +437,7 @@ export interface IWatchOptions { * watching. If not provided, all paths are considered for * events. */ - includes?: string[]; + includes?: Array<string | IRelativePattern>; } export const enum FileSystemProviderCapabilities { diff --git a/src/vs/platform/files/common/watcher.ts b/src/vs/platform/files/common/watcher.ts index 78614c324dc..57277a9ece9 100644 --- a/src/vs/platform/files/common/watcher.ts +++ b/src/vs/platform/files/common/watcher.ts @@ -4,7 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { Event } from 'vs/base/common/event'; +import { GLOBSTAR, IRelativePattern, parse, ParsedPattern } from 'vs/base/common/glob'; import { Disposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; +import { isAbsolute } from 'vs/base/common/path'; import { isLinux } from 'vs/base/common/platform'; import { URI as uri } from 'vs/base/common/uri'; import { FileChangeType, IFileChange, isParent } from 'vs/platform/files/common/files'; @@ -23,6 +25,11 @@ interface IWatchRequest { /** * A set of glob patterns or paths to exclude from watching. + * + * Paths or basic glob patterns that are relative will be + * resolved to an absolute path using the currently opened + * workspace. Complex glob patterns must match on absolute + * paths via leading or trailing `**`. */ excludes: string[]; @@ -30,8 +37,13 @@ interface IWatchRequest { * An optional set of glob patterns or paths to include for * watching. If not provided, all paths are considered for * events. + * + * Paths or basic glob patterns that are relative will be + * resolved to an absolute path using the currently opened + * workspace. Complex glob patterns must match on absolute + * paths via leading or trailing `**`. */ - includes?: string[]; + includes?: Array<string | IRelativePattern>; } export interface INonRecursiveWatchRequest extends IWatchRequest { @@ -279,6 +291,26 @@ export function coalesceEvents(changes: IDiskFileChange[]): IDiskFileChange[] { return coalescer.coalesce(); } +export function parseWatcherPatterns(path: string, patterns: Array<string | IRelativePattern>): ParsedPattern[] { + const parsedPatterns: ParsedPattern[] = []; + + for (const pattern of patterns) { + let normalizedPattern = pattern; + + // Patterns are always matched on the full absolute path + // of the event. As such, if the pattern is not absolute + // and does not start with a leading `**`, we have to + // convert it to a relative pattern with the given `base` + if (typeof normalizedPattern === 'string' && !normalizedPattern.startsWith(GLOBSTAR) && !isAbsolute(normalizedPattern)) { + normalizedPattern = { base: path, pattern: normalizedPattern }; + } + + parsedPatterns.push(parse(normalizedPattern)); + } + + return parsedPatterns; +} + class EventCoalescer { private readonly coalesced = new Set<IDiskFileChange>(); diff --git a/src/vs/platform/files/node/watcher/nodejs/nodejsWatcher.ts b/src/vs/platform/files/node/watcher/nodejs/nodejsWatcher.ts index df6bd3f016c..a737ca54510 100644 --- a/src/vs/platform/files/node/watcher/nodejs/nodejsWatcher.ts +++ b/src/vs/platform/files/node/watcher/nodejs/nodejsWatcher.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { equals } from 'vs/base/common/arrays'; import { Event, Emitter } from 'vs/base/common/event'; +import { patternsEquals } from 'vs/base/common/glob'; import { Disposable } from 'vs/base/common/lifecycle'; import { isLinux } from 'vs/base/common/platform'; import { IDiskFileChange, ILogMessage, INonRecursiveWatchRequest, INonRecursiveWatcher } from 'vs/platform/files/common/watcher'; @@ -50,12 +50,12 @@ export class NodeJSWatcher extends Disposable implements INonRecursiveWatcher { } // Re-watch path if excludes or includes have changed - return !equals(watcher.request.excludes, request.excludes) || !equals(watcher.request.includes, request.includes); + return !patternsEquals(watcher.request.excludes, request.excludes) || !patternsEquals(watcher.request.includes, request.includes); }); // Gather paths that we should stop watching const pathsToStopWatching = Array.from(this.watchers.values()).filter(({ request }) => { - return !normalizedRequests.find(normalizedRequest => normalizedRequest.path === request.path && equals(normalizedRequest.excludes, request.excludes) && equals(normalizedRequest.includes, request.includes)); + return !normalizedRequests.find(normalizedRequest => normalizedRequest.path === request.path && patternsEquals(normalizedRequest.excludes, request.excludes) && patternsEquals(normalizedRequest.includes, request.includes)); }).map(({ request }) => request.path); // Logging diff --git a/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts b/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts index c23ea589dbb..77a3ed87dda 100644 --- a/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts +++ b/src/vs/platform/files/node/watcher/nodejs/nodejsWatcherLib.ts @@ -7,7 +7,6 @@ import { watch } from 'fs'; import { ThrottledDelayer, ThrottledWorker } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { isEqualOrParent } from 'vs/base/common/extpath'; -import { parse } from 'vs/base/common/glob'; import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { normalizeNFC } from 'vs/base/common/normalization'; import { basename, dirname, join } from 'vs/base/common/path'; @@ -15,7 +14,7 @@ import { isLinux, isMacintosh } from 'vs/base/common/platform'; import { realcase } from 'vs/base/node/extpath'; import { Promises } from 'vs/base/node/pfs'; import { FileChangeType } from 'vs/platform/files/common/files'; -import { IDiskFileChange, ILogMessage, coalesceEvents, INonRecursiveWatchRequest } from 'vs/platform/files/common/watcher'; +import { IDiskFileChange, ILogMessage, coalesceEvents, INonRecursiveWatchRequest, parseWatcherPatterns } from 'vs/platform/files/common/watcher'; export class NodeJSFileWatcherLibrary extends Disposable { @@ -47,8 +46,8 @@ export class NodeJSFileWatcherLibrary extends Disposable { private readonly fileChangesDelayer = this._register(new ThrottledDelayer<void>(NodeJSFileWatcherLibrary.FILE_CHANGES_HANDLER_DELAY)); private fileChangesBuffer: IDiskFileChange[] = []; - private readonly excludes = this.request.excludes.map(exclude => parse(exclude)); - private readonly includes = this.request.includes?.map(include => parse(include)); + private readonly excludes = parseWatcherPatterns(this.request.path, this.request.excludes); + private readonly includes = this.request.includes ? parseWatcherPatterns(this.request.path, this.request.includes) : undefined; private readonly cts = new CancellationTokenSource(); @@ -100,7 +99,7 @@ export class NodeJSFileWatcherLibrary extends Disposable { // Correct watch path as needed if (request.path !== realPath) { - this.warn(`correcting a path to watch that seems to be a symbolic link or wrong casing (original: ${request.path}, real: ${realPath})`); + this.trace(`correcting a path to watch that seems to be a symbolic link or wrong casing (original: ${request.path}, real: ${realPath})`); } } catch (error) { // ignore diff --git a/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts b/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts index 85388db8f80..4b9c801252b 100644 --- a/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts +++ b/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts @@ -11,7 +11,7 @@ import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cance import { toErrorMessage } from 'vs/base/common/errorMessage'; import { Emitter } from 'vs/base/common/event'; import { isEqualOrParent, randomPath } from 'vs/base/common/extpath'; -import { parse, ParsedPattern } from 'vs/base/common/glob'; +import { ParsedPattern, patternsEquals } from 'vs/base/common/glob'; import { Disposable } from 'vs/base/common/lifecycle'; import { TernarySearchTree } from 'vs/base/common/map'; import { normalizeNFC } from 'vs/base/common/normalization'; @@ -21,8 +21,7 @@ import { rtrim } from 'vs/base/common/strings'; import { realcaseSync, realpathSync } from 'vs/base/node/extpath'; import { NodeJSFileWatcherLibrary } from 'vs/platform/files/node/watcher/nodejs/nodejsWatcherLib'; import { FileChangeType } from 'vs/platform/files/common/files'; -import { IDiskFileChange, ILogMessage, coalesceEvents, IRecursiveWatchRequest, IRecursiveWatcher } from 'vs/platform/files/common/watcher'; -import { equals } from 'vs/base/common/arrays'; +import { IDiskFileChange, ILogMessage, coalesceEvents, IRecursiveWatchRequest, IRecursiveWatcher, parseWatcherPatterns } from 'vs/platform/files/common/watcher'; export interface IParcelWatcherInstance { @@ -129,15 +128,15 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { } // Re-watch path if excludes/includes have changed or polling interval - return !equals(watcher.request.excludes, request.excludes) || !equals(watcher.request.includes, request.includes) || watcher.request.pollingInterval !== request.pollingInterval; + return !patternsEquals(watcher.request.excludes, request.excludes) || !patternsEquals(watcher.request.includes, request.includes) || watcher.request.pollingInterval !== request.pollingInterval; }); // Gather paths that we should stop watching const pathsToStopWatching = Array.from(this.watchers.values()).filter(({ request }) => { return !normalizedRequests.find(normalizedRequest => { return normalizedRequest.path === request.path && - equals(normalizedRequest.excludes, request.excludes) && - equals(normalizedRequest.includes, request.includes) && + patternsEquals(normalizedRequest.excludes, request.excludes) && + patternsEquals(normalizedRequest.includes, request.includes) && normalizedRequest.pollingInterval === request.pollingInterval; }); @@ -285,8 +284,8 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { const { realPath, realPathDiffers, realPathLength } = this.normalizePath(request); // Warm up exclude/include patterns for usage - const excludePatterns = request.excludes.map(exclude => parse(exclude)); - const includePatterns = request.includes?.map(include => parse(include)); + const excludePatterns = parseWatcherPatterns(request.path, request.excludes); + const includePatterns = request.includes ? parseWatcherPatterns(request.path, request.includes) : undefined; const ignore = this.toExcludePaths(realPath, watcher.request.excludes); @@ -355,8 +354,8 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { const { realPath, realPathDiffers, realPathLength } = this.normalizePath(request); // Warm up exclude/include patterns for usage - const excludePatterns = request.excludes.map(exclude => parse(exclude)); - const includePatterns = request.includes?.map(include => parse(include)); + const excludePatterns = parseWatcherPatterns(request.path, request.excludes); + const includePatterns = request.includes ? parseWatcherPatterns(request.path, request.includes) : undefined; const ignore = this.toExcludePaths(realPath, watcher.request.excludes); parcelWatcher.subscribe(realPath, (error, parcelEvents) => { @@ -393,14 +392,16 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { return; } - // Check for excludes - const rawEvents = this.handleExcludeIncludes(parcelEvents, excludes, includes); - // Normalize events: handle NFC normalization and symlinks - const { events: normalizedEvents, rootDeleted } = this.normalizeEvents(rawEvents, watcher.request, realPathDiffers, realPathLength); + // It is important to do this before checking for includes + // and excludes to check on the original path. + const { events: normalizedEvents, rootDeleted } = this.normalizeEvents(parcelEvents, watcher.request, realPathDiffers, realPathLength); + + // Check for excludes + const includedEvents = this.handleExcludeIncludes(normalizedEvents, excludes, includes); // Coalesce events: merge events of same kind - const coalescedEvents = coalesceEvents(normalizedEvents); + const coalescedEvents = coalesceEvents(includedEvents); // Filter events: check for specific events we want to exclude const filteredEvents = this.filterEvents(coalescedEvents, watcher.request, rootDeleted); @@ -486,7 +487,7 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { realPathLength = realPath.length; realPathDiffers = true; - this.warn(`correcting a path to watch that seems to be a symbolic link or wrong casing (original: ${request.path}, real: ${realPath})`); + this.trace(`correcting a path to watch that seems to be a symbolic link or wrong casing (original: ${request.path}, real: ${realPath})`); } } catch (error) { // ignore @@ -495,7 +496,7 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { return { realPath, realPathDiffers, realPathLength }; } - private normalizeEvents(events: IDiskFileChange[], request: IRecursiveWatchRequest, realPathDiffers: boolean, realPathLength: number): { events: IDiskFileChange[]; rootDeleted: boolean } { + private normalizeEvents(events: parcelWatcher.Event[], request: IRecursiveWatchRequest, realPathDiffers: boolean, realPathLength: number): { events: parcelWatcher.Event[]; rootDeleted: boolean } { let rootDeleted = false; for (const event of events) { @@ -519,7 +520,7 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { } // Check for root deleted - if (event.path === request.path && event.type === FileChangeType.DELETED) { + if (event.path === request.path && event.type === 'delete') { rootDeleted = true; } } @@ -672,7 +673,7 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { try { const realpath = realpathSync(request.path); if (realpath === request.path) { - this.warn(`ignoring a path for watching who's parent is already watched: ${request.path}`); + this.trace(`ignoring a path for watching who's parent is already watched: ${request.path}`); continue; // path is not a symbolic link or similar } diff --git a/src/vs/platform/files/test/node/watcherCoalescer.test.ts b/src/vs/platform/files/test/common/watcher.test.ts index e6bef2015b5..ed4bf455193 100644 --- a/src/vs/platform/files/test/node/watcherCoalescer.test.ts +++ b/src/vs/platform/files/test/common/watcher.test.ts @@ -9,7 +9,7 @@ import { isLinux, isWindows } from 'vs/base/common/platform'; import { isEqual } from 'vs/base/common/resources'; import { URI as uri } from 'vs/base/common/uri'; import { FileChangesEvent, FileChangeType, IFileChange } from 'vs/platform/files/common/files'; -import { IDiskFileChange, coalesceEvents, toFileChanges } from 'vs/platform/files/common/watcher'; +import { IDiskFileChange, coalesceEvents, toFileChanges, parseWatcherPatterns } from 'vs/platform/files/common/watcher'; class TestFileWatcher { private readonly _onDidFilesChange: Emitter<{ raw: IFileChange[]; event: FileChangesEvent }>; @@ -48,6 +48,63 @@ enum Path { UNC } +suite('Watcher', () => { + + (isWindows ? test.skip : test)('parseWatcherPatterns - posix', () => { + const path = '/users/data/src'; + let parsedPattern = parseWatcherPatterns(path, ['*.js'])[0]; + + assert.strictEqual(parsedPattern('/users/data/src/foo.js'), true); + assert.strictEqual(parsedPattern('/users/data/src/foo.ts'), false); + assert.strictEqual(parsedPattern('/users/data/src/bar/foo.js'), false); + + parsedPattern = parseWatcherPatterns(path, ['/users/data/src/*.js'])[0]; + + assert.strictEqual(parsedPattern('/users/data/src/foo.js'), true); + assert.strictEqual(parsedPattern('/users/data/src/foo.ts'), false); + assert.strictEqual(parsedPattern('/users/data/src/bar/foo.js'), false); + + parsedPattern = parseWatcherPatterns(path, ['/users/data/src/bar/*.js'])[0]; + + assert.strictEqual(parsedPattern('/users/data/src/foo.js'), false); + assert.strictEqual(parsedPattern('/users/data/src/foo.ts'), false); + assert.strictEqual(parsedPattern('/users/data/src/bar/foo.js'), true); + + parsedPattern = parseWatcherPatterns(path, ['**/*.js'])[0]; + + assert.strictEqual(parsedPattern('/users/data/src/foo.js'), true); + assert.strictEqual(parsedPattern('/users/data/src/foo.ts'), false); + assert.strictEqual(parsedPattern('/users/data/src/bar/foo.js'), true); + }); + + (!isWindows ? test.skip : test)('parseWatcherPatterns - windows', () => { + const path = 'c:\\users\\data\\src'; + let parsedPattern = parseWatcherPatterns(path, ['*.js'])[0]; + + assert.strictEqual(parsedPattern('c:\\users\\data\\src\\foo.js'), true); + assert.strictEqual(parsedPattern('c:\\users\\data\\src\\foo.ts'), false); + assert.strictEqual(parsedPattern('c:\\users\\data\\src\\bar/foo.js'), false); + + parsedPattern = parseWatcherPatterns(path, ['c:\\users\\data\\src\\*.js'])[0]; + + assert.strictEqual(parsedPattern('c:\\users\\data\\src\\foo.js'), true); + assert.strictEqual(parsedPattern('c:\\users\\data\\src\\foo.ts'), false); + assert.strictEqual(parsedPattern('c:\\users\\data\\src\\bar\\foo.js'), false); + + parsedPattern = parseWatcherPatterns(path, ['c:\\users\\data\\src\\bar/*.js'])[0]; + + assert.strictEqual(parsedPattern('c:\\users\\data\\src\\foo.js'), false); + assert.strictEqual(parsedPattern('c:\\users\\data\\src\\foo.ts'), false); + assert.strictEqual(parsedPattern('c:\\users\\data\\src\\bar\\foo.js'), true); + + parsedPattern = parseWatcherPatterns(path, ['**/*.js'])[0]; + + assert.strictEqual(parsedPattern('c:\\users\\data\\src\\foo.js'), true); + assert.strictEqual(parsedPattern('c:\\users\\data\\src\\foo.ts'), false); + assert.strictEqual(parsedPattern('c:\\users\\data\\src\\bar\\foo.js'), true); + }); +}); + suite('Watcher Events Normalizer', () => { test('simple add/update/delete', done => { diff --git a/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts b/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts index d617ce2b2e4..d6e69b14663 100644 --- a/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts +++ b/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts @@ -392,6 +392,18 @@ import { NodeJSWatcher } from 'vs/platform/files/node/watcher/nodejs/nodejsWatch return basicCrudTest(join(testDir, 'files-includes.txt')); }); + test('includes are supported (folder watch, relative pattern explicit)', async function () { + await watcher.watch([{ path: testDir, excludes: [], includes: [{ base: testDir, pattern: 'files-includes.txt' }], recursive: false }]); + + return basicCrudTest(join(testDir, 'files-includes.txt')); + }); + + test('includes are supported (folder watch, relative pattern implicit)', async function () { + await watcher.watch([{ path: testDir, excludes: [], includes: ['files-includes.txt'], recursive: false }]); + + return basicCrudTest(join(testDir, 'files-includes.txt')); + }); + (isWindows /* windows: cannot create file symbolic link without elevated context */ ? test.skip : test)('symlink support (folder watch)', async function () { const link = join(testDir, 'deep-linked'); const linkTarget = join(testDir, 'deep'); diff --git a/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts b/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts index 5529a525e0c..0c12d6371e6 100644 --- a/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts +++ b/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts @@ -450,6 +450,18 @@ import { ltrim } from 'vs/base/common/strings'; return basicCrudTest(join(testDir, 'deep', 'newFile.txt')); }); + test('includes are supported (relative pattern explicit)', async function () { + await watcher.watch([{ path: testDir, excludes: [], includes: [{ base: testDir, pattern: 'deep/newFile.txt' }], recursive: true }]); + + return basicCrudTest(join(testDir, 'deep', 'newFile.txt')); + }); + + test('includes are supported (relative pattern implicit)', async function () { + await watcher.watch([{ path: testDir, excludes: [], includes: ['deep/newFile.txt'], recursive: true }]); + + return basicCrudTest(join(testDir, 'deep', 'newFile.txt')); + }); + (isWindows /* windows: cannot create file symbolic link without elevated context */ ? test.skip : test)('symlink support (root)', async function () { const link = join(testDir, 'deep-linked'); const linkTarget = join(testDir, 'deep'); diff --git a/src/vs/platform/issue/electron-main/issueMainService.ts b/src/vs/platform/issue/electron-main/issueMainService.ts index 828e7ae65b3..a61b652845a 100644 --- a/src/vs/platform/issue/electron-main/issueMainService.ts +++ b/src/vs/platform/issue/electron-main/issueMainService.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BrowserWindow, BrowserWindowConstructorOptions, Display, ipcMain, IpcMainEvent, screen } from 'electron'; +import { BrowserWindow, BrowserWindowConstructorOptions, Display, IpcMainEvent, screen } from 'electron'; +import { validatedIpcMain } from 'vs/base/parts/ipc/electron-main/ipcMain'; import { arch, release, type } from 'os'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { DisposableStore } from 'vs/base/common/lifecycle'; @@ -65,14 +66,14 @@ export class IssueMainService implements ICommonIssueService { } private registerListeners(): void { - ipcMain.on('vscode:issueSystemInfoRequest', async event => { + validatedIpcMain.on('vscode:issueSystemInfoRequest', async event => { const [info, remoteData] = await Promise.all([this.launchMainService.getMainProcessInfo(), this.diagnosticsMainService.getRemoteDiagnostics({ includeProcesses: false, includeWorkspaceMetadata: false })]); const msg = await this.diagnosticsService.getSystemInfo(info, remoteData); this.safeSend(event, 'vscode:issueSystemInfoResponse', msg); }); - ipcMain.on('vscode:listProcesses', async event => { + validatedIpcMain.on('vscode:listProcesses', async event => { const processes = []; try { @@ -102,7 +103,7 @@ export class IssueMainService implements ICommonIssueService { this.safeSend(event, 'vscode:listProcessesResponse', processes); }); - ipcMain.on('vscode:issueReporterClipboard', async event => { + validatedIpcMain.on('vscode:issueReporterClipboard', async event => { const messageOptions = { title: this.productService.nameLong, message: localize('issueReporterWriteToClipboard', "There is too much data to send to GitHub directly. The data will be copied to the clipboard, please paste it into the GitHub issue page that is opened."), @@ -122,12 +123,12 @@ export class IssueMainService implements ICommonIssueService { } }); - ipcMain.on('vscode:issuePerformanceInfoRequest', async event => { + validatedIpcMain.on('vscode:issuePerformanceInfoRequest', async event => { const performanceInfo = await this.getPerformanceInfo(); this.safeSend(event, 'vscode:issuePerformanceInfoResponse', performanceInfo); }); - ipcMain.on('vscode:issueReporterConfirmClose', async () => { + validatedIpcMain.on('vscode:issueReporterConfirmClose', async () => { const messageOptions = { title: this.productService.nameLong, message: localize('confirmCloseIssueReporter', "Your input will not be saved. Are you sure you want to close this window?"), @@ -152,7 +153,7 @@ export class IssueMainService implements ICommonIssueService { } }); - ipcMain.on('vscode:workbenchCommand', (_: unknown, commandInfo: { id: any; from: any; args: any }) => { + validatedIpcMain.on('vscode:workbenchCommand', (_: unknown, commandInfo: { id: any; from: any; args: any }) => { const { id, from, args } = commandInfo; let parentWindow: BrowserWindow | null; @@ -172,23 +173,23 @@ export class IssueMainService implements ICommonIssueService { } }); - ipcMain.on('vscode:openExternal', (_: unknown, arg: string) => { + validatedIpcMain.on('vscode:openExternal', (_: unknown, arg: string) => { this.nativeHostMainService.openExternal(undefined, arg); }); - ipcMain.on('vscode:closeIssueReporter', event => { + validatedIpcMain.on('vscode:closeIssueReporter', event => { if (this.issueReporterWindow) { this.issueReporterWindow.close(); } }); - ipcMain.on('vscode:closeProcessExplorer', event => { + validatedIpcMain.on('vscode:closeProcessExplorer', event => { if (this.processExplorerWindow) { this.processExplorerWindow.close(); } }); - ipcMain.on('vscode:windowsInfoRequest', async event => { + validatedIpcMain.on('vscode:windowsInfoRequest', async event => { const mainProcessInfo = await this.launchMainService.getMainProcessInfo(); this.safeSend(event, 'vscode:windowsInfoResponse', mainProcessInfo.windows); }); diff --git a/src/vs/platform/label/common/label.ts b/src/vs/platform/label/common/label.ts index df4c1c91bc2..a89fb9d0c48 100644 --- a/src/vs/platform/label/common/label.ts +++ b/src/vs/platform/label/common/label.ts @@ -21,7 +21,7 @@ export interface ILabelService { * If `noPrefix` is passed does not tildify the label and also does not prepand the root name for relative labels in a multi root scenario. * If `separator` is passed, will use that over the defined path separator of the formatter. */ - getUriLabel(resource: URI, options?: { relative?: boolean; noPrefix?: boolean; endWithSeparator?: boolean; separator?: '/' | '\\' }): string; + getUriLabel(resource: URI, options?: { relative?: boolean; noPrefix?: boolean; separator?: '/' | '\\' }): string; getUriBasenameLabel(resource: URI): string; getWorkspaceLabel(workspace: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI | IWorkspace), options?: { verbose: boolean }): string; getHostLabel(scheme: string, authority?: string): string; diff --git a/src/vs/platform/lifecycle/electron-main/lifecycleMainService.ts b/src/vs/platform/lifecycle/electron-main/lifecycleMainService.ts index ac3b7bfb3b2..ae26de3e981 100644 --- a/src/vs/platform/lifecycle/electron-main/lifecycleMainService.ts +++ b/src/vs/platform/lifecycle/electron-main/lifecycleMainService.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { app, BrowserWindow, ipcMain } from 'electron'; +import { app, BrowserWindow } from 'electron'; +import { validatedIpcMain } from 'vs/base/parts/ipc/electron-main/ipcMain'; import { Barrier, Promises, timeout } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; @@ -511,11 +512,11 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe const okChannel = `vscode:ok${oneTimeEventToken}`; const cancelChannel = `vscode:cancel${oneTimeEventToken}`; - ipcMain.once(okChannel, () => { + validatedIpcMain.once(okChannel, () => { resolve(false); // no veto }); - ipcMain.once(cancelChannel, () => { + validatedIpcMain.once(cancelChannel, () => { resolve(true); // veto }); @@ -528,7 +529,7 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe const oneTimeEventToken = this.oneTimeListenerTokenGenerator++; const replyChannel = `vscode:reply${oneTimeEventToken}`; - ipcMain.once(replyChannel, () => resolve()); + validatedIpcMain.once(replyChannel, () => resolve()); window.send('vscode:onWillUnload', { replyChannel, reason }); }); diff --git a/src/vs/platform/menubar/electron-main/menubar.ts b/src/vs/platform/menubar/electron-main/menubar.ts index c4f612d801f..9fd8cca89ea 100644 --- a/src/vs/platform/menubar/electron-main/menubar.ts +++ b/src/vs/platform/menubar/electron-main/menubar.ts @@ -3,11 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { app, BrowserWindow, KeyboardEvent, Menu, MenuItem, MenuItemConstructorOptions, WebContents } from 'electron'; +import { app, BrowserWindow, KeyboardEvent, Menu, MenuItem, MenuItemConstructorOptions, MessageBoxOptions, WebContents } from 'electron'; import { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions'; import { RunOnceScheduler } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { mnemonicMenuLabel } from 'vs/base/common/labels'; +import { mnemonicButtonLabel, mnemonicMenuLabel } from 'vs/base/common/labels'; import { isMacintosh, language } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import * as nls from 'vs/nls'; @@ -118,6 +118,7 @@ export class Menubar { this.fallbackMenuHandlers['workbench.action.files.newUntitledFile'] = (menuItem, win, event) => this.windowsMainService.openEmptyWindow({ context: OpenContext.MENU, contextWindowId: win?.id }); this.fallbackMenuHandlers['workbench.action.newWindow'] = (menuItem, win, event) => this.windowsMainService.openEmptyWindow({ context: OpenContext.MENU, contextWindowId: win?.id }); this.fallbackMenuHandlers['workbench.action.files.openFileFolder'] = (menuItem, win, event) => this.nativeHostMainService.pickFileFolderAndOpen(undefined, { forceNewWindow: this.isOptionClick(event), telemetryExtraData: { from: telemetryFrom } }); + this.fallbackMenuHandlers['workbench.action.files.openFolder'] = (menuItem, win, event) => this.nativeHostMainService.pickFolderAndOpen(undefined, { forceNewWindow: this.isOptionClick(event), telemetryExtraData: { from: telemetryFrom } }); this.fallbackMenuHandlers['workbench.action.openWorkspace'] = (menuItem, win, event) => this.nativeHostMainService.pickWorkspaceAndOpen(undefined, { forceNewWindow: this.isOptionClick(event), telemetryExtraData: { from: telemetryFrom } }); // Recent Menu Items @@ -160,6 +161,7 @@ export class Menubar { } private registerListeners(): void { + // Keep flag when app quits this.lifecycleMainService.onWillShutdown(() => this.willShutdown = true); @@ -382,14 +384,17 @@ export class Menubar { const hideOthers = new MenuItem({ label: nls.localize('mHideOthers', "Hide Others"), role: 'hideOthers', accelerator: 'Command+Alt+H' }); const showAll = new MenuItem({ label: nls.localize('mShowAll', "Show All"), role: 'unhide' }); const quit = new MenuItem(this.likeAction('workbench.action.quit', { - label: nls.localize('miQuit', "Quit {0}", this.productService.nameLong), click: () => { + label: nls.localize('miQuit', "Quit {0}", this.productService.nameLong), click: async (item, window, event) => { const lastActiveWindow = this.windowsMainService.getLastActiveWindow(); if ( this.windowsMainService.getWindowCount() === 0 || // allow to quit when no more windows are open !!BrowserWindow.getFocusedWindow() || // allow to quit when window has focus (fix for https://github.com/microsoft/vscode/issues/39191) lastActiveWindow?.isMinimized() // allow to quit when window has no focus but is minimized (https://github.com/microsoft/vscode/issues/63000) ) { - this.nativeHostMainService.quit(undefined); + const confirmed = await this.confirmBeforeQuit(event); + if (confirmed) { + this.nativeHostMainService.quit(undefined); + } } } })); @@ -418,6 +423,33 @@ export class Menubar { actions.forEach(i => macApplicationMenu.append(i)); } + private async confirmBeforeQuit(event: KeyboardEvent): Promise<boolean> { + if (this.windowsMainService.getWindowCount() === 0) { + return true; // never confirm when no windows are opened + } + + const confirmBeforeClose = this.configurationService.getValue<'always' | 'never' | 'keyboardOnly'>('window.confirmBeforeClose'); + if (confirmBeforeClose === 'always' || (confirmBeforeClose === 'keyboardOnly' && this.isKeyboardEvent(event))) { + const options: MessageBoxOptions = { + title: this.productService.nameLong, + type: 'question', + buttons: [ + mnemonicButtonLabel(nls.localize({ key: 'quit', comment: ['&& denotes a mnemonic'] }, "&&Quit")), + mnemonicButtonLabel(nls.localize({ key: 'cancel', comment: ['&& denotes a mnemonic'] }, "&&Cancel")) + ], + message: nls.localize('quitMessage', "Are you sure you want to quit?"), + noLink: true, + defaultId: 0, + cancelId: 1 + }; + + const { response } = await this.nativeHostMainService.showMessageBox(this.windowsMainService.getFocusedWindow()?.id, options); + return response === 0; + } + + return true; + } + private shouldDrawMenu(menuId: string): boolean { // We need to draw an empty menu to override the electron default if (!isMacintosh && getTitleBarStyle(this.configurationService) === 'custom') { @@ -522,6 +554,10 @@ export class Menubar { return !!(event && ((!isMacintosh && (event.ctrlKey || event.shiftKey)) || (isMacintosh && (event.metaKey || event.altKey)))); } + private isKeyboardEvent(event: KeyboardEvent): boolean { + return !!(event.triggeredByAccelerator || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey); + } + private createRoleMenuItem(label: string, commandId: string, role: any): MenuItem { const options: MenuItemConstructorOptions = { label: this.mnemonicLabel(label), diff --git a/src/vs/platform/native/common/native.ts b/src/vs/platform/native/common/native.ts index f0d57b347d5..c5cfd92a41c 100644 --- a/src/vs/platform/native/common/native.ts +++ b/src/vs/platform/native/common/native.ts @@ -71,6 +71,8 @@ export interface ICommonNativeHostService { unmaximizeWindow(): Promise<void>; minimizeWindow(): Promise<void>; + updateTitleBarOverlay(backgroundColor: string, foregroundColor: string): Promise<void>; + setMinimumSize(width: number | undefined, height: number | undefined): Promise<void>; saveWindowSplash(splash: IPartsSplash): Promise<void>; @@ -153,6 +155,7 @@ export interface ICommonNativeHostService { // Connectivity resolveProxy(url: string): Promise<string | undefined>; + findFreePort(startPort: number, giveUpAfter: number, timeout: number, stride?: number): Promise<number>; // Registry (windows only) windowsGetStringRegKey(hive: 'HKEY_CURRENT_USER' | 'HKEY_LOCAL_MACHINE' | 'HKEY_CLASSES_ROOT' | 'HKEY_USERS' | 'HKEY_CURRENT_CONFIG', path: string, name: string): Promise<string | undefined>; diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index ac91aa14727..c167d9011a9 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -19,6 +19,7 @@ import { URI } from 'vs/base/common/uri'; import { realpath } from 'vs/base/node/extpath'; import { virtualMachineHint } from 'vs/base/node/id'; import { Promises, SymlinkSupport } from 'vs/base/node/pfs'; +import { findFreePort } from 'vs/base/node/ports'; import { MouseInputEvent } from 'vs/base/parts/sandbox/common/electronTypes'; import { localize } from 'vs/nls'; import { ISerializableCommandAction } from 'vs/platform/action/common/action'; @@ -211,6 +212,20 @@ export class NativeHostMainService extends Disposable implements INativeHostMain } } + async updateTitleBarOverlay(windowId: number | undefined, backgroundColor: string, foregroundColor: string): Promise<void> { + if (!isWindows) { + return; // Windows only + } + + const window = this.windowById(windowId); + if (window?.win) { + window.win.setTitleBarOverlay({ + color: backgroundColor, + symbolColor: foregroundColor + }); + } + } + async focusWindow(windowId: number | undefined, options?: { windowId?: number; force?: boolean }): Promise<void> { if (options && typeof options.windowId === 'number') { windowId = options.windowId; @@ -736,6 +751,10 @@ export class NativeHostMainService extends Disposable implements INativeHostMain } } + findFreePort(windowId: number | undefined, startPort: number, giveUpAfter: number, timeout: number, stride = 1): Promise<number> { + return findFreePort(startPort, giveUpAfter, timeout, stride); + } + //#endregion diff --git a/src/vs/platform/product/common/product.ts b/src/vs/platform/product/common/product.ts index 5f8c4bcc101..7e63a164e2c 100644 --- a/src/vs/platform/product/common/product.ts +++ b/src/vs/platform/product/common/product.ts @@ -58,7 +58,7 @@ else { // Running out of sources if (Object.keys(product).length === 0) { Object.assign(product, { - version: '1.66.0-dev', + version: '1.67.0-dev', nameShort: 'Code - OSS Dev', nameLong: 'Code - OSS Dev', applicationName: 'code-oss', diff --git a/src/vs/platform/protocol/electron-main/protocolMainService.ts b/src/vs/platform/protocol/electron-main/protocolMainService.ts index 324c8abbece..e414b377c6e 100644 --- a/src/vs/platform/protocol/electron-main/protocolMainService.ts +++ b/src/vs/platform/protocol/electron-main/protocolMainService.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ipcMain, session } from 'electron'; +import { session } from 'electron'; +import { validatedIpcMain } from 'vs/base/parts/ipc/electron-main/ipcMain'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { TernarySearchTree } from 'vs/base/common/map'; import { FileAccess, Schemas } from 'vs/base/common/network'; @@ -138,7 +139,7 @@ export class ProtocolMainService extends Disposable implements IProtocolMainServ // Install IPC handler const channel = resource.toString(); const handler = async (): Promise<T | undefined> => obj; - ipcMain.handle(channel, handler); + validatedIpcMain.handle(channel, handler); this.logService.trace(`IPC Object URL: Registered new channel ${channel}.`); @@ -148,7 +149,7 @@ export class ProtocolMainService extends Disposable implements IProtocolMainServ dispose: () => { this.logService.trace(`IPC Object URL: Removed channel ${channel}.`); - ipcMain.removeHandler(channel); + validatedIpcMain.removeHandler(channel); } }; } diff --git a/src/vs/platform/remote/test/electron-sandbox/remoteAuthorityResolverService.test.ts b/src/vs/platform/remote/test/electron-sandbox/remoteAuthorityResolverService.test.ts new file mode 100644 index 00000000000..81f17b25ed2 --- /dev/null +++ b/src/vs/platform/remote/test/electron-sandbox/remoteAuthorityResolverService.test.ts @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { RemoteAuthorityResolverError, RemoteAuthorityResolverErrorCode } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { RemoteAuthorityResolverService } from 'vs/platform/remote/electron-sandbox/remoteAuthorityResolverService'; + +suite('RemoteAuthorityResolverService', () => { + test('issue #147318: RemoteAuthorityResolverError keeps the same type', async () => { + const service = new RemoteAuthorityResolverService(); + const result = service.resolveAuthority('test+x'); + service._setResolvedAuthorityError('test+x', new RemoteAuthorityResolverError('something', RemoteAuthorityResolverErrorCode.TemporarilyNotAvailable)); + try { + await result; + assert.fail(); + } catch (err) { + assert.strictEqual(RemoteAuthorityResolverError.isTemporarilyNotAvailable(err), true); + } + }); +}); diff --git a/src/vs/platform/request/browser/requestService.ts b/src/vs/platform/request/browser/requestService.ts index 56971e949eb..09c9baec136 100644 --- a/src/vs/platform/request/browser/requestService.ts +++ b/src/vs/platform/request/browser/requestService.ts @@ -24,14 +24,24 @@ export class RequestService implements IRequestService { ) { } - request(options: IRequestOptions, token: CancellationToken): Promise<IRequestContext> { - this.logService.trace('RequestService#request', options.url); + async request(options: IRequestOptions, token: CancellationToken): Promise<IRequestContext> { + this.logService.trace('RequestService#request (browser) - begin', options.url); if (!options.proxyAuthorization) { options.proxyAuthorization = this.configurationService.getValue<string>('http.proxyAuthorization'); } - return request(options, token); + try { + const res = await request(options, token); + + this.logService.trace('RequestService#request (browser) - success', options.url); + + return res; + } catch (error) { + this.logService.error('RequestService#request (browser) - error', options.url, error); + + throw error; + } } async resolveProxy(url: string): Promise<string | undefined> { diff --git a/src/vs/platform/request/common/request.ts b/src/vs/platform/request/common/request.ts index d217ccb1f67..15f9d6d94d0 100644 --- a/src/vs/platform/request/common/request.ts +++ b/src/vs/platform/request/common/request.ts @@ -30,9 +30,6 @@ function hasNoContent(context: IRequestContext): boolean { } export async function asText(context: IRequestContext): Promise<string | null> { - if (!isSuccess(context)) { - throw new Error('Server returned ' + context.res.statusCode); - } if (hasNoContent(context)) { return null; } @@ -40,6 +37,13 @@ export async function asText(context: IRequestContext): Promise<string | null> { return buffer.toString(); } +export async function asTextOrError(context: IRequestContext): Promise<string | null> { + if (!isSuccess(context)) { + throw new Error('Server returned ' + context.res.statusCode); + } + return asText(context); +} + export async function asJson<T = {}>(context: IRequestContext): Promise<T | null> { if (!isSuccess(context)) { throw new Error('Server returned ' + context.res.statusCode); diff --git a/src/vs/platform/request/node/requestService.ts b/src/vs/platform/request/node/requestService.ts index 3196fe1fcfc..042ae6be1b5 100644 --- a/src/vs/platform/request/node/requestService.ts +++ b/src/vs/platform/request/node/requestService.ts @@ -62,7 +62,7 @@ export class RequestService extends Disposable implements IRequestService { } async request(options: NodeRequestOptions, token: CancellationToken): Promise<IRequestContext> { - this.logService.trace('RequestService#request', options.url); + this.logService.trace('RequestService#request (node) - begin', options.url); const { proxyUrl, strictSSL } = this; @@ -72,7 +72,7 @@ export class RequestService extends Disposable implements IRequestService { } catch (error) { if (!this.shellEnvErrorLogged) { this.shellEnvErrorLogged = true; - this.logService.error('RequestService#request resolving shell environment failed', error); + this.logService.error('RequestService#request (node) resolving shell environment failed', error); } } @@ -92,7 +92,17 @@ export class RequestService extends Disposable implements IRequestService { }; } - return this._request(options, token); + try { + const res = await this._request(options, token); + + this.logService.trace('RequestService#request (node) - success', options.url); + + return res; + } catch (error) { + this.logService.trace('RequestService#request (node) - error', options.url, error); + + throw error; + } } private async getNodeRequest(options: IRequestOptions): Promise<IRawRequestFunction> { diff --git a/src/vs/platform/sharedProcess/electron-browser/sharedProcessWorkerMain.ts b/src/vs/platform/sharedProcess/electron-browser/sharedProcessWorkerMain.ts index f0af8c6648e..165268c5e12 100644 --- a/src/vs/platform/sharedProcess/electron-browser/sharedProcessWorkerMain.ts +++ b/src/vs/platform/sharedProcess/electron-browser/sharedProcessWorkerMain.ts @@ -12,7 +12,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { deepClone } from 'vs/base/common/objects'; import { withNullAsUndefined } from 'vs/base/common/types'; -import { removeDangerousEnvVariables } from 'vs/base/node/processes'; +import { removeDangerousEnvVariables } from 'vs/base/common/processes'; import { hash, ISharedProcessWorkerConfiguration, ISharedProcessWorkerProcessExit } from 'vs/platform/sharedProcess/common/sharedProcessWorkerService'; import { SharedProcessWorkerMessages, ISharedProcessToWorkerMessage, ISharedProcessWorkerEnvironment, IWorkerToSharedProcessMessage } from 'vs/platform/sharedProcess/electron-browser/sharedProcessWorker'; diff --git a/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts b/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts index 3d2122e121d..f7046a4d756 100644 --- a/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts +++ b/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BrowserWindow, Event as ElectronEvent, ipcMain, IpcMainEvent, MessagePortMain } from 'electron'; +import { BrowserWindow, Event as ElectronEvent, IpcMainEvent, MessagePortMain } from 'electron'; +import { validatedIpcMain } from 'vs/base/parts/ipc/electron-main/ipcMain'; import { Barrier } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; @@ -49,10 +50,10 @@ export class SharedProcess extends Disposable implements ISharedProcess { private registerListeners(): void { // Shared process connections from workbench windows - ipcMain.on('vscode:createSharedProcessMessageChannel', (e, nonce: string) => this.onWindowConnection(e, nonce)); + validatedIpcMain.on('vscode:createSharedProcessMessageChannel', (e, nonce: string) => this.onWindowConnection(e, nonce)); // Shared process worker relay - ipcMain.on('vscode:relaySharedProcessWorkerMessageChannel', (e, configuration: ISharedProcessWorkerConfiguration) => this.onWorkerConnection(e, configuration)); + validatedIpcMain.on('vscode:relaySharedProcessWorkerMessageChannel', (e, configuration: ISharedProcessWorkerConfiguration) => this.onWorkerConnection(e, configuration)); // Lifecycle this._register(this.lifecycleMainService.onWillShutdown(() => this.onWillShutdown())); @@ -173,7 +174,7 @@ export class SharedProcess extends Disposable implements ISharedProcess { if (!this._whenReady) { // Overall signal that the shared process window was loaded and // all services within have been created. - this._whenReady = new Promise<void>(resolve => ipcMain.once('vscode:shared-process->electron-main=init-done', () => { + this._whenReady = new Promise<void>(resolve => validatedIpcMain.once('vscode:shared-process->electron-main=init-done', () => { this.logService.trace('SharedProcess: Overall ready'); resolve(); @@ -198,7 +199,7 @@ export class SharedProcess extends Disposable implements ISharedProcess { this.registerWindowListeners(); // Wait for window indicating that IPC connections are accepted - await new Promise<void>(resolve => ipcMain.once('vscode:shared-process->electron-main=ipc-ready', () => { + await new Promise<void>(resolve => validatedIpcMain.once('vscode:shared-process->electron-main=ipc-ready', () => { this.logService.trace('SharedProcess: IPC ready'); resolve(); diff --git a/src/vs/platform/storage/common/storageIpc.ts b/src/vs/platform/storage/common/storageIpc.ts index e6916d0c230..5774cb82085 100644 --- a/src/vs/platform/storage/common/storageIpc.ts +++ b/src/vs/platform/storage/common/storageIpc.ts @@ -86,8 +86,9 @@ class GlobalStorageDatabaseClient extends BaseStorageDatabaseClient implements I async close(): Promise<void> { // The global storage database is shared across all instances so - // we do not await it. However we dispose the listener for external - // changes because we no longer interested int it. + // we do not close it from the window. However we dispose the + // listener for external changes because we no longer interested in it. + this.dispose(); } } @@ -101,9 +102,12 @@ class WorkspaceStorageDatabaseClient extends BaseStorageDatabaseClient implement } async close(): Promise<void> { - const serializableRequest: ISerializableUpdateRequest = { workspace: this.workspace }; - return this.channel.call('close', serializableRequest); + // The workspace storage database is only used in this instance + // but we do not need to close it from here, the main process + // can take care of that. + + this.dispose(); } } diff --git a/src/vs/platform/storage/electron-main/storageIpc.ts b/src/vs/platform/storage/electron-main/storageIpc.ts index 87c392c46fd..9802b767d91 100644 --- a/src/vs/platform/storage/electron-main/storageIpc.ts +++ b/src/vs/platform/storage/electron-main/storageIpc.ts @@ -105,18 +105,6 @@ export class StorageDatabaseChannel extends Disposable implements IServerChannel break; } - case 'close': { - - // We only allow to close workspace scoped storage because - // global storage is shared across all windows and closes - // only on shutdown. - if (workspace) { - return storage.close(); - } - - break; - } - default: throw new Error(`Call not found: ${command}`); } diff --git a/src/vs/platform/storage/electron-main/storageMain.ts b/src/vs/platform/storage/electron-main/storageMain.ts index 0143fd29652..384deb88135 100644 --- a/src/vs/platform/storage/electron-main/storageMain.ts +++ b/src/vs/platform/storage/electron-main/storageMain.ts @@ -212,41 +212,51 @@ abstract class BaseStorageMain extends Disposable implements IStorageMain { // a chance that the underlying DB is large // either on disk or in general. In that case // log some additional info to further diagnose - if (watch.elapsed() > BaseStorageMain.LOG_SLOW_CLOSE_THRESHOLD && this.path) { - try { - const largestEntries = top(Array.from(this._storage.items.entries()) - .map(([key, value]) => ({ key, length: value.length })), (entryA, entryB) => entryB.length - entryA.length, 5) - .map(entry => `${entry.key}:${entry.length}`).join(', '); - const dbSize = (await this.fileService.stat(URI.file(this.path))).size; - - this.logService.warn(`[storage main] detected slow close() operation: Time: ${watch.elapsed()}ms, DB size: ${dbSize}b, Large Keys: ${largestEntries}`); - - type StorageSlowCloseClassification = { - duration: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; owner: 'bpasero'; comment: 'The time it took to close the DB in ms.' }; - size: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; owner: 'bpasero'; comment: 'The size of the DB in bytes.' }; - largestEntries: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; owner: 'bpasero'; comment: 'The 5 largest keys in the DB.' }; - }; - - type StorageSlowCloseEvent = { - duration: number; - size: number; - largestEntries: string; - }; - - this.telemetryService.publicLog2<StorageSlowCloseEvent, StorageSlowCloseClassification>('storageSlowClose', { - duration: watch.elapsed(), - size: dbSize, - largestEntries - }); - } catch (error) { - this.logService.error('[storage main] figuring out stats for slow DB on close() resulted in an error', error); - } + if (watch.elapsed() > BaseStorageMain.LOG_SLOW_CLOSE_THRESHOLD) { + await this.logSlowClose(watch); } // Signal as event this._onDidCloseStorage.fire(); } + private async logSlowClose(watch: StopWatch) { + if (!this.path) { + return; + } + + try { + const largestEntries = top(Array.from(this._storage.items.entries()) + .map(([key, value]) => ({ key, length: value.length })), (entryA, entryB) => entryB.length - entryA.length, 5) + .map(entry => `${entry.key}:${entry.length}`).join(', '); + const dbSize = (await this.fileService.stat(URI.file(this.path))).size; + + this.logService.warn(`[storage main] detected slow close() operation: Time: ${watch.elapsed()}ms, DB size: ${dbSize}b, Large Keys: ${largestEntries}`); + + type StorageSlowCloseClassification = { + duration: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'The time it took to close the DB in ms.' }; + size: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'The size of the DB in bytes.' }; + largestEntries: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'The 5 largest keys in the DB.' }; + owner: 'bpasero'; + comment: 'Used to gain insight into reasons a database may be slow. This is used to assist with further optimizations'; + }; + + type StorageSlowCloseEvent = { + duration: number; + size: number; + largestEntries: string; + }; + + this.telemetryService.publicLog2<StorageSlowCloseEvent, StorageSlowCloseClassification>('storageSlowClose', { + duration: watch.elapsed(), + size: dbSize, + largestEntries + }); + } catch (error) { + this.logService.error('[storage main] figuring out stats for slow DB on close() resulted in an error', error); + } + } + private async doClose(): Promise<void> { // Ensure we are not accidentally leaving diff --git a/src/vs/platform/storage/electron-main/storageMainService.ts b/src/vs/platform/storage/electron-main/storageMainService.ts index 6781158c321..aee6d47d758 100644 --- a/src/vs/platform/storage/electron-main/storageMainService.ts +++ b/src/vs/platform/storage/electron-main/storageMainService.ts @@ -76,7 +76,7 @@ export class StorageMainService extends Disposable implements IStorageMainServic })(); // Workspace Storage: Warmup when related window with workspace loads - this._register(this.lifecycleMainService.onWillLoadWindow(async e => { + this._register(this.lifecycleMainService.onWillLoadWindow(e => { if (e.workspace) { this.workspaceStorage(e.workspace).init(); } @@ -142,9 +142,11 @@ export class StorageMainService extends Disposable implements IStorageMainServic private createWorkspaceStorage(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier): IStorageMain { if (this.shutdownReason === ShutdownReason.KILL) { + // Workaround for native crashes that we see when // SQLite DBs are being created even after shutdown // https://github.com/microsoft/vscode/issues/143186 + return new InMemoryStorageMain(this.logService, this.fileService, this.telemetryService); } diff --git a/src/vs/platform/telemetry/common/gdprTypings.ts b/src/vs/platform/telemetry/common/gdprTypings.ts index 316c84fca8d..e14d6fb900d 100644 --- a/src/vs/platform/telemetry/common/gdprTypings.ts +++ b/src/vs/platform/telemetry/common/gdprTypings.ts @@ -5,7 +5,6 @@ export interface IPropertyData { classification: 'SystemMetaData' | 'CallstackOrException' | 'CustomerContent' | 'PublicNonPersonalData' | 'EndUserPseudonymizedInformation'; purpose: 'PerformanceAndHealth' | 'FeatureInsight' | 'BusinessInsight'; - owner?: string; comment?: string; expiration?: string; endpoint?: string; @@ -13,17 +12,22 @@ export interface IPropertyData { } export interface IGDPRProperty { - readonly [name: string]: IPropertyData | undefined | IGDPRProperty; + owner?: string; + comment?: string; + expiration?: string; + readonly [name: string]: IPropertyData | undefined | IGDPRProperty | string; } +type IGDPRPropertyWithoutMetadata<T> = Omit<T, 'owner' | 'comment' | 'expiration'>; + export type ClassifiedEvent<T extends IGDPRProperty> = { - [k in keyof T]: any + [k in keyof IGDPRPropertyWithoutMetadata<T>]: any }; export type StrictPropertyChecker<TEvent, TClassifiedEvent, TError> = keyof TEvent extends keyof TClassifiedEvent ? keyof TClassifiedEvent extends keyof TEvent ? TEvent : TError : TError; -export type StrictPropertyCheckError = 'Type of classified event does not match event properties'; +export type StrictPropertyCheckError = { error: 'Type of classified event does not match event properties' }; export type StrictPropertyCheck<T extends IGDPRProperty, E> = StrictPropertyChecker<E, ClassifiedEvent<T>, StrictPropertyCheckError>; -export type GDPRClassification<T> = { [_ in keyof T]: IPropertyData | IGDPRProperty | undefined }; +export type GDPRClassification<T> = { [_ in keyof T]: IPropertyData | IGDPRProperty | undefined | string }; diff --git a/src/vs/platform/telemetry/common/serverTelemetryService.ts b/src/vs/platform/telemetry/common/serverTelemetryService.ts index 48dfb1cc8fe..74d12ea8086 100644 --- a/src/vs/platform/telemetry/common/serverTelemetryService.ts +++ b/src/vs/platform/telemetry/common/serverTelemetryService.ts @@ -15,22 +15,14 @@ export interface IServerTelemetryService extends ITelemetryService { updateInjectedTelemetryLevel(telemetryLevel: TelemetryLevel): Promise<void>; } -interface CachedTelemetryEvent { - eventName: string; - data?: ITelemetryData; - anonymizeFilePaths?: boolean; - eventType: 'usage' | 'error'; -} - export class ServerTelemetryService extends TelemetryService implements IServerTelemetryService { - private _telemetryCache: CachedTelemetryEvent[] = []; // Because we cannot read the workspace config on the remote site // the ServerTelemetryService is responsible for knowing its telemetry level // this is done through IPC calls and initial value injections - private _injectedTelemetryLevel: TelemetryLevel | undefined; + private _injectedTelemetryLevel: TelemetryLevel; constructor( config: ITelemetryServiceConfig, - injectedTelemetryLevel: TelemetryLevel | undefined, + injectedTelemetryLevel: TelemetryLevel, @IConfigurationService _configurationService: IConfigurationService, @IProductService _productService: IProductService ) { @@ -39,11 +31,6 @@ export class ServerTelemetryService extends TelemetryService implements IServerT } override publicLog(eventName: string, data?: ITelemetryData, anonymizeFilePaths?: boolean): Promise<void> { - if (this._injectedTelemetryLevel === undefined) { - // Undefined safety with cache in case super class calls log before cache is initialized in subclass constructor - this._telemetryCache?.push({ eventName, data, anonymizeFilePaths, eventType: 'usage' }); - return Promise.resolve(); - } if (this._injectedTelemetryLevel < TelemetryLevel.USAGE) { return Promise.resolve(undefined); } @@ -55,11 +42,6 @@ export class ServerTelemetryService extends TelemetryService implements IServerT } override publicLogError(errorEventName: string, data?: ITelemetryData): Promise<void> { - if (this._injectedTelemetryLevel === undefined) { - // Undefined safety with cache in case super class calls log before cache is initialized in subclass constructor - this._telemetryCache?.push({ eventName: errorEventName, data, eventType: 'error' }); - return Promise.resolve(); - } if (this._injectedTelemetryLevel < TelemetryLevel.ERROR) { return Promise.resolve(undefined); } @@ -70,21 +52,6 @@ export class ServerTelemetryService extends TelemetryService implements IServerT return this.publicLogError(eventName, data as ITelemetryData | undefined); } - // Flushes all the cached events with the new level - async flushTelemetryCache(): Promise<void> { - if (this._telemetryCache?.length === 0) { - return; - } - for (const cacheItem of this._telemetryCache) { - if (cacheItem.eventType === 'usage') { - await this.publicLog(cacheItem.eventName, cacheItem.data, cacheItem.anonymizeFilePaths); - } else { - await this.publicLogError(cacheItem.eventName, cacheItem.data); - } - } - this._telemetryCache = []; - } - async updateInjectedTelemetryLevel(telemetryLevel: TelemetryLevel): Promise<void> { if (telemetryLevel === undefined) { this._injectedTelemetryLevel = TelemetryLevel.NONE; @@ -93,11 +60,7 @@ export class ServerTelemetryService extends TelemetryService implements IServerT // We always take the most restrictive level because we don't want multiple clients to connect and send data when one client does not consent this._injectedTelemetryLevel = this._injectedTelemetryLevel ? Math.min(this._injectedTelemetryLevel, telemetryLevel) : telemetryLevel; if (this._injectedTelemetryLevel === TelemetryLevel.NONE) { - this._telemetryCache = []; this.dispose(); - } else { - // Level was set we're no longer in a pending state we flush the telemetry cache. - return this.flushTelemetryCache(); } } } diff --git a/src/vs/platform/telemetry/common/telemetry.ts b/src/vs/platform/telemetry/common/telemetry.ts index be381cd790c..1577949b1e2 100644 --- a/src/vs/platform/telemetry/common/telemetry.ts +++ b/src/vs/platform/telemetry/common/telemetry.ts @@ -5,6 +5,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ClassifiedEvent, GDPRClassification, StrictPropertyCheck } from 'vs/platform/telemetry/common/gdprTypings'; +import { IObservableValue } from 'vs/base/common/observableValue'; export const ITelemetryService = createDecorator<ITelemetryService>('telemetryService'); @@ -46,7 +47,7 @@ export interface ITelemetryService { setExperimentProperty(name: string, value: string): void; - telemetryLevel: TelemetryLevel; + readonly telemetryLevel: IObservableValue<TelemetryLevel>; } export interface ITelemetryEndpoint { diff --git a/src/vs/platform/telemetry/common/telemetryService.ts b/src/vs/platform/telemetry/common/telemetryService.ts index f16e3f0d240..adcec1d16f6 100644 --- a/src/vs/platform/telemetry/common/telemetryService.ts +++ b/src/vs/platform/telemetry/common/telemetryService.ts @@ -5,6 +5,7 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { cloneAndChange, mixin } from 'vs/base/common/objects'; +import { MutableObservableValue } from 'vs/base/common/observableValue'; import { isWeb } from 'vs/base/common/platform'; import { escapeRegExpCharacters } from 'vs/base/common/strings'; import { localize } from 'vs/nls'; @@ -35,9 +36,10 @@ export class TelemetryService implements ITelemetryService { private _commonProperties: Promise<{ [name: string]: any }>; private _experimentProperties: { [name: string]: string } = {}; private _piiPaths: string[]; - private _telemetryLevel: TelemetryLevel; private _sendErrorTelemetry: boolean; + public readonly telemetryLevel = new MutableObservableValue<TelemetryLevel>(TelemetryLevel.USAGE); + private readonly _disposables = new DisposableStore(); private _cleanupPatterns: RegExp[] = []; @@ -49,7 +51,6 @@ export class TelemetryService implements ITelemetryService { this._appenders = config.appenders; this._commonProperties = config.commonProperties || Promise.resolve({}); this._piiPaths = config.piiPaths || []; - this._telemetryLevel = TelemetryLevel.USAGE; this._sendErrorTelemetry = !!config.sendErrorTelemetry; // static cleanup pattern for: `file:///DANGEROUS/PATH/resources/app/Useful/Information` @@ -59,7 +60,6 @@ export class TelemetryService implements ITelemetryService { this._cleanupPatterns.push(new RegExp(escapeRegExpCharacters(piiPath), 'gi')); } - this._updateTelemetryLevel(); this._configurationService.onDidChangeConfiguration(this._updateTelemetryLevel, this, this._disposables); } @@ -69,19 +69,17 @@ export class TelemetryService implements ITelemetryService { } private _updateTelemetryLevel(): void { - this._telemetryLevel = getTelemetryLevel(this._configurationService); + let level = getTelemetryLevel(this._configurationService); const collectableTelemetry = this._productService.enabledTelemetryLevels; // Also ensure that error telemetry is respecting the product configuration for collectable telemetry if (collectableTelemetry) { this._sendErrorTelemetry = this.sendErrorTelemetry ? collectableTelemetry.error : false; // Make sure the telemetry level from the service is the minimum of the config and product const maxCollectableTelemetryLevel = collectableTelemetry.usage ? TelemetryLevel.USAGE : collectableTelemetry.error ? TelemetryLevel.ERROR : TelemetryLevel.NONE; - this._telemetryLevel = Math.min(this._telemetryLevel, maxCollectableTelemetryLevel); + level = Math.min(level, maxCollectableTelemetryLevel); } - } - get telemetryLevel(): TelemetryLevel { - return this._telemetryLevel; + this.telemetryLevel.value = level; } get sendErrorTelemetry(): boolean { @@ -106,7 +104,7 @@ export class TelemetryService implements ITelemetryService { private _log(eventName: string, eventLevel: TelemetryLevel, data?: ITelemetryData, anonymizeFilePaths?: boolean): Promise<any> { // don't send events when the user is optout - if (this.telemetryLevel < eventLevel) { + if (this.telemetryLevel.value < eventLevel) { return Promise.resolve(undefined); } @@ -201,17 +199,18 @@ export class TelemetryService implements ITelemetryService { const value = property.toLowerCase(); - const emailRegex = /@[a-zA-Z0-9-.]+/; // Regex which matches @*.site - const secretRegex = /(key|token|sig|signature|password|passwd|pwd|android:value)[^a-zA-Z0-9]/; - const tokenRegex = /xox[pbaors]\-[a-zA-Z0-9]+\-[a-zA-Z0-9\-]+?/; // last +? is lazy as a microoptimization since we don't care about the full value + const userDataRegexes = [ + { label: 'Google API Key', regex: /AIza[A-Za-z0-9_\\\-]{35}/ }, + { label: 'Slack Token', regex: /xox[pbar]\-[A-Za-z0-9]/ }, + { label: 'Generic Secret', regex: /(key|token|sig|secret|signature|password|passwd|pwd|android:value)[^a-zA-Z0-9]/ }, + { label: 'Email', regex: /@[a-zA-Z0-9-.]+/ } // Regex which matches @*.site + ]; // Check for common user data in the telemetry events - if (secretRegex.test(value)) { - return '<REDACTED: secret>'; - } else if (emailRegex.test(value)) { - return '<REDACTED: email>'; - } else if (tokenRegex.test(value)) { - return '<REDACTED: token>'; + for (const secretRegex of userDataRegexes) { + if (secretRegex.regex.test(value)) { + return `<REDACTED: ${secretRegex.label}`; + } } return property; diff --git a/src/vs/platform/telemetry/common/telemetryUtils.ts b/src/vs/platform/telemetry/common/telemetryUtils.ts index d74b997e1dd..45757fec68b 100644 --- a/src/vs/platform/telemetry/common/telemetryUtils.ts +++ b/src/vs/platform/telemetry/common/telemetryUtils.ts @@ -5,6 +5,7 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { safeStringify } from 'vs/base/common/objects'; +import { staticObservableValue } from 'vs/base/common/observableValue'; import { isObject } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { ConfigurationTarget, ConfigurationTargetToString, IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -31,7 +32,7 @@ export class NullTelemetryServiceShape implements ITelemetryService { } setExperimentProperty() { } - telemetryLevel = TelemetryLevel.NONE; + telemetryLevel = staticObservableValue(TelemetryLevel.NONE); getTelemetryInfo(): Promise<ITelemetryInfo> { return Promise.resolve({ instanceId: 'someValue.instanceId', diff --git a/src/vs/platform/telemetry/test/browser/telemetryService.test.ts b/src/vs/platform/telemetry/test/browser/telemetryService.test.ts index 9fbe21f1057..808db0e4a82 100644 --- a/src/vs/platform/telemetry/test/browser/telemetryService.test.ts +++ b/src/vs/platform/telemetry/test/browser/telemetryService.test.ts @@ -800,15 +800,15 @@ suite('TelemetryService', () => { } }(), TestProductService); - assert.strictEqual(service.telemetryLevel, TelemetryLevel.NONE); + assert.strictEqual(service.telemetryLevel.value, TelemetryLevel.NONE); telemetryLevel = TelemetryConfiguration.ON; emitter.fire({}); - assert.strictEqual(service.telemetryLevel, TelemetryLevel.USAGE); + assert.strictEqual(service.telemetryLevel.value, TelemetryLevel.USAGE); telemetryLevel = TelemetryConfiguration.ERROR; emitter.fire({}); - assert.strictEqual(service.telemetryLevel, TelemetryLevel.ERROR); + assert.strictEqual(service.telemetryLevel.value, TelemetryLevel.ERROR); service.dispose(); }); diff --git a/src/vs/platform/terminal/node/ptyHostService.ts b/src/vs/platform/terminal/node/ptyHostService.ts index 9bba0c3f537..b7679d75f90 100644 --- a/src/vs/platform/terminal/node/ptyHostService.ts +++ b/src/vs/platform/terminal/node/ptyHostService.ts @@ -104,8 +104,8 @@ export class PtyHostService extends Disposable implements IPtyService { })); } - async initialize(): Promise<void> { - await this._refreshIgnoreProcessNames(); + initialize(): void { + this._refreshIgnoreProcessNames(); } private get _ignoreProcessNames(): string[] { diff --git a/src/vs/platform/terminal/node/terminalEnvironment.ts b/src/vs/platform/terminal/node/terminalEnvironment.ts index dcbff469770..a679186010f 100644 --- a/src/vs/platform/terminal/node/terminalEnvironment.ts +++ b/src/vs/platform/terminal/node/terminalEnvironment.ts @@ -12,6 +12,7 @@ import * as process from 'vs/base/common/process'; import { format } from 'vs/base/common/strings'; import { isString } from 'vs/base/common/types'; import * as pfs from 'vs/base/node/pfs'; +import { ILogService } from 'vs/platform/log/common/log'; import { IShellLaunchConfig, ITerminalProcessOptions } from 'vs/platform/terminal/common/terminal'; export function getWindowsBuildNumber(): number { @@ -102,13 +103,14 @@ export interface IShellIntegrationConfigInjection { */ export function getShellIntegrationInjection( shellLaunchConfig: IShellLaunchConfig, - options: ITerminalProcessOptions['shellIntegration'] + options: ITerminalProcessOptions['shellIntegration'], + logService: ILogService ): IShellIntegrationConfigInjection | undefined { // Shell integration arg injection is disabled when: // - The global setting is disabled // - There is no executable (not sure what script to run) // - The terminal is used by a feature like tasks or debugging - if (!options.enabled || !shellLaunchConfig.executable || shellLaunchConfig.isFeatureTerminal) { + if (!options.enabled || !shellLaunchConfig.executable || shellLaunchConfig.isFeatureTerminal || shellLaunchConfig.hideFromUser) { return undefined; } @@ -135,6 +137,7 @@ export function getShellIntegrationInjection( } return { newArgs }; } + logService.warn(`Shell integration cannot be enabled for executable "${shellLaunchConfig.executable}" and args`, shellLaunchConfig.args); return undefined; } @@ -193,13 +196,21 @@ export function getShellIntegrationInjection( source: path.join(appRoot, 'out/vs/workbench/contrib/terminal/browser/media/shellIntegration.zsh'), dest: path.join(zdotdir, '.zshrc') }); + filesToCopy.push({ + source: path.join(appRoot, 'out/vs/workbench/contrib/terminal/browser/media/shellIntegration-profile.zsh'), + dest: path.join(zdotdir, '.zprofile') + }); + filesToCopy.push({ + source: path.join(appRoot, 'out/vs/workbench/contrib/terminal/browser/media/shellIntegration-env.zsh'), + dest: path.join(zdotdir, '.zshenv') + }); if (!options.showWelcome) { envMixin['VSCODE_SHELL_HIDE_WELCOME'] = '1'; } return { newArgs, envMixin, filesToCopy }; } } - + logService.warn(`Shell integration cannot be enabled for executable "${shellLaunchConfig.executable}" and args`, shellLaunchConfig.args); return undefined; } diff --git a/src/vs/platform/terminal/node/terminalProcess.ts b/src/vs/platform/terminal/node/terminalProcess.ts index a7c1d444d4e..4b4be142397 100644 --- a/src/vs/platform/terminal/node/terminalProcess.ts +++ b/src/vs/platform/terminal/node/terminalProcess.ts @@ -198,11 +198,9 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess } let injection: IShellIntegrationConfigInjection | undefined; - if (this._options.shellIntegration) { - injection = getShellIntegrationInjection(this.shellLaunchConfig, this._options.shellIntegration); - if (!injection) { - this._logService.warn(`Shell integration cannot be enabled for executable "${this.shellLaunchConfig.executable}" and args`, this.shellLaunchConfig.args); - } else { + if (this._options.shellIntegration.enabled) { + injection = getShellIntegrationInjection(this.shellLaunchConfig, this._options.shellIntegration, this._logService); + if (injection) { if (injection.envMixin) { for (const [key, value] of Object.entries(injection.envMixin)) { this._ptyOptions.env ||= {}; diff --git a/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts b/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts index 84fb8a7e08d..8dffe31a645 100644 --- a/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts +++ b/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { deepStrictEqual, ok, strictEqual } from 'assert'; +import { NullLogService } from 'vs/platform/log/common/log'; import { ITerminalProcessOptions } from 'vs/platform/terminal/common/terminal'; import { getShellIntegrationInjection, IShellIntegrationConfigInjection } from 'vs/platform/terminal/node/terminalEnvironment'; @@ -11,13 +12,14 @@ const enabledProcessOptions: ITerminalProcessOptions['shellIntegration'] = { ena const disabledProcessOptions: ITerminalProcessOptions['shellIntegration'] = { enabled: false, showWelcome: true }; const pwshExe = process.platform === 'win32' ? 'pwsh.exe' : 'pwsh'; const repoRoot = process.platform === 'win32' ? process.cwd()[0].toLowerCase() + process.cwd().substring(1) : process.cwd(); +const logService = new NullLogService(); suite('platform - terminalEnvironment', () => { suite('getShellIntegrationInjection', () => { suite('should not enable', () => { test('when isFeatureTerminal or when no executable is provided', () => { - ok(!getShellIntegrationInjection({ executable: pwshExe, args: ['-l', '-NoLogo'], isFeatureTerminal: true }, enabledProcessOptions)); - ok(getShellIntegrationInjection({ executable: pwshExe, args: ['-l', '-NoLogo'], isFeatureTerminal: false }, enabledProcessOptions)); + ok(!getShellIntegrationInjection({ executable: pwshExe, args: ['-l', '-NoLogo'], isFeatureTerminal: true }, enabledProcessOptions, logService)); + ok(getShellIntegrationInjection({ executable: pwshExe, args: ['-l', '-NoLogo'], isFeatureTerminal: false }, enabledProcessOptions, logService)); }); }); @@ -34,21 +36,21 @@ suite('platform - terminalEnvironment', () => { ] }); test('when undefined, []', () => { - deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: [] }, enabledProcessOptions), enabledExpectedResult); - deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: undefined }, enabledProcessOptions), enabledExpectedResult); + deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: [] }, enabledProcessOptions, logService), enabledExpectedResult); + deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: undefined }, enabledProcessOptions, logService), enabledExpectedResult); }); suite('when no logo', () => { test('array - case insensitive', () => { - deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: ['-NoLogo'] }, enabledProcessOptions), enabledExpectedResult); - deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: ['-NOLOGO'] }, enabledProcessOptions), enabledExpectedResult); - deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: ['-nol'] }, enabledProcessOptions), enabledExpectedResult); - deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: ['-NOL'] }, enabledProcessOptions), enabledExpectedResult); + deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: ['-NoLogo'] }, enabledProcessOptions, logService), enabledExpectedResult); + deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: ['-NOLOGO'] }, enabledProcessOptions, logService), enabledExpectedResult); + deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: ['-nol'] }, enabledProcessOptions, logService), enabledExpectedResult); + deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: ['-NOL'] }, enabledProcessOptions, logService), enabledExpectedResult); }); test('string - case insensitive', () => { - deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: '-NoLogo' }, enabledProcessOptions), enabledExpectedResult); - deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: '-NOLOGO' }, enabledProcessOptions), enabledExpectedResult); - deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: '-nol' }, enabledProcessOptions), enabledExpectedResult); - deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: '-NOL' }, enabledProcessOptions), enabledExpectedResult); + deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: '-NoLogo' }, enabledProcessOptions, logService), enabledExpectedResult); + deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: '-NOLOGO' }, enabledProcessOptions, logService), enabledExpectedResult); + deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: '-nol' }, enabledProcessOptions, logService), enabledExpectedResult); + deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: '-NOL' }, enabledProcessOptions, logService), enabledExpectedResult); }); }); }); @@ -62,23 +64,23 @@ suite('platform - terminalEnvironment', () => { ] }); test('when array contains no logo and login', () => { - deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: ['-l', '-NoLogo'] }, enabledProcessOptions), enabledExpectedResult); + deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: ['-l', '-NoLogo'] }, enabledProcessOptions, logService), enabledExpectedResult); }); test('when string', () => { - deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: '-l' }, enabledProcessOptions), enabledExpectedResult); + deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: '-l' }, enabledProcessOptions, logService), enabledExpectedResult); }); }); suite('should not modify args', () => { test('when shell integration is disabled', () => { - strictEqual(getShellIntegrationInjection({ executable: pwshExe, args: ['-l'] }, disabledProcessOptions), undefined); - strictEqual(getShellIntegrationInjection({ executable: pwshExe, args: '-l' }, disabledProcessOptions), undefined); - strictEqual(getShellIntegrationInjection({ executable: pwshExe, args: undefined }, disabledProcessOptions), undefined); + strictEqual(getShellIntegrationInjection({ executable: pwshExe, args: ['-l'] }, disabledProcessOptions, logService), undefined); + strictEqual(getShellIntegrationInjection({ executable: pwshExe, args: '-l' }, disabledProcessOptions, logService), undefined); + strictEqual(getShellIntegrationInjection({ executable: pwshExe, args: undefined }, disabledProcessOptions, logService), undefined); }); test('when using unrecognized arg', () => { - strictEqual(getShellIntegrationInjection({ executable: pwshExe, args: ['-l', '-NoLogo', '-i'] }, disabledProcessOptions), undefined); + strictEqual(getShellIntegrationInjection({ executable: pwshExe, args: ['-l', '-NoLogo', '-i'] }, disabledProcessOptions, logService), undefined); }); test('when using unrecognized arg (string)', () => { - strictEqual(getShellIntegrationInjection({ executable: pwshExe, args: '-i' }, disabledProcessOptions), undefined); + strictEqual(getShellIntegrationInjection({ executable: pwshExe, args: '-i' }, disabledProcessOptions, logService), undefined); }); }); }); @@ -87,37 +89,45 @@ suite('platform - terminalEnvironment', () => { suite('zsh', () => { suite('should override args', () => { const expectedDir = /.+\/vscode-zsh/; - const expectedDest = /.+\/vscode-zsh\/.zshrc/; - const expectedSource = /.+\/out\/vs\/workbench\/contrib\/terminal\/browser\/media\/shellIntegration.zsh/; + const expectedDests = [/.+\/vscode-zsh\/.zshrc/, /.+\/vscode-zsh\/.zprofile/, /.+\/vscode-zsh\/.zshenv/]; + const expectedSources = [ + /.+\/out\/vs\/workbench\/contrib\/terminal\/browser\/media\/shellIntegration.zsh/, + /.+\/out\/vs\/workbench\/contrib\/terminal\/browser\/media\/shellIntegration-profile.zsh/, + /.+\/out\/vs\/workbench\/contrib\/terminal\/browser\/media\/shellIntegration-env.zsh/ + ]; function assertIsEnabled(result: IShellIntegrationConfigInjection) { strictEqual(Object.keys(result.envMixin!).length, 1); ok(result.envMixin!['ZDOTDIR']?.match(expectedDir)); - strictEqual(result.filesToCopy?.length, 1); - ok(result.filesToCopy[0].dest.match(expectedDest)); - ok(result.filesToCopy[0].source.match(expectedSource)); + strictEqual(result.filesToCopy?.length, 3); + ok(result.filesToCopy[0].dest.match(expectedDests[0])); + ok(result.filesToCopy[1].dest.match(expectedDests[1])); + ok(result.filesToCopy[2].dest.match(expectedDests[2])); + ok(result.filesToCopy[0].source.match(expectedSources[0])); + ok(result.filesToCopy[1].source.match(expectedSources[1])); + ok(result.filesToCopy[2].source.match(expectedSources[2])); } test('when undefined, []', () => { - const result1 = getShellIntegrationInjection({ executable: 'zsh', args: [] }, enabledProcessOptions); + const result1 = getShellIntegrationInjection({ executable: 'zsh', args: [] }, enabledProcessOptions, logService); deepStrictEqual(result1?.newArgs, ['-i']); assertIsEnabled(result1); - const result2 = getShellIntegrationInjection({ executable: 'zsh', args: undefined }, enabledProcessOptions); + const result2 = getShellIntegrationInjection({ executable: 'zsh', args: undefined }, enabledProcessOptions, logService); deepStrictEqual(result2?.newArgs, ['-i']); assertIsEnabled(result2); }); suite('should incorporate login arg', () => { test('when array', () => { - const result = getShellIntegrationInjection({ executable: 'zsh', args: ['-l'] }, enabledProcessOptions); + const result = getShellIntegrationInjection({ executable: 'zsh', args: ['-l'] }, enabledProcessOptions, logService); deepStrictEqual(result?.newArgs, ['-il']); assertIsEnabled(result); }); }); suite('should not modify args', () => { test('when shell integration is disabled', () => { - strictEqual(getShellIntegrationInjection({ executable: 'zsh', args: ['-l'] }, disabledProcessOptions), undefined); - strictEqual(getShellIntegrationInjection({ executable: 'zsh', args: undefined }, disabledProcessOptions), undefined); + strictEqual(getShellIntegrationInjection({ executable: 'zsh', args: ['-l'] }, disabledProcessOptions, logService), undefined); + strictEqual(getShellIntegrationInjection({ executable: 'zsh', args: undefined }, disabledProcessOptions, logService), undefined); }); test('when using unrecognized arg', () => { - strictEqual(getShellIntegrationInjection({ executable: 'zsh', args: ['-l', '-fake'] }, disabledProcessOptions), undefined); + strictEqual(getShellIntegrationInjection({ executable: 'zsh', args: ['-l', '-fake'] }, disabledProcessOptions, logService), undefined); }); }); }); @@ -132,9 +142,9 @@ suite('platform - terminalEnvironment', () => { ], envMixin: {} }); - deepStrictEqual(getShellIntegrationInjection({ executable: 'bash', args: [] }, enabledProcessOptions), enabledExpectedResult); - deepStrictEqual(getShellIntegrationInjection({ executable: 'bash', args: '' }, enabledProcessOptions), enabledExpectedResult); - deepStrictEqual(getShellIntegrationInjection({ executable: 'bash', args: undefined }, enabledProcessOptions), enabledExpectedResult); + deepStrictEqual(getShellIntegrationInjection({ executable: 'bash', args: [] }, enabledProcessOptions, logService), enabledExpectedResult); + deepStrictEqual(getShellIntegrationInjection({ executable: 'bash', args: '' }, enabledProcessOptions, logService), enabledExpectedResult); + deepStrictEqual(getShellIntegrationInjection({ executable: 'bash', args: undefined }, enabledProcessOptions, logService), enabledExpectedResult); }); suite('should set login env variable and not modify args', () => { const enabledExpectedResult = Object.freeze<IShellIntegrationConfigInjection>({ @@ -147,16 +157,16 @@ suite('platform - terminalEnvironment', () => { } }); test('when array', () => { - deepStrictEqual(getShellIntegrationInjection({ executable: 'bash', args: ['-l'] }, enabledProcessOptions), enabledExpectedResult); + deepStrictEqual(getShellIntegrationInjection({ executable: 'bash', args: ['-l'] }, enabledProcessOptions, logService), enabledExpectedResult); }); }); suite('should not modify args', () => { test('when shell integration is disabled', () => { - strictEqual(getShellIntegrationInjection({ executable: 'bash', args: ['-l'] }, disabledProcessOptions), undefined); - strictEqual(getShellIntegrationInjection({ executable: 'bash', args: undefined }, disabledProcessOptions), undefined); + strictEqual(getShellIntegrationInjection({ executable: 'bash', args: ['-l'] }, disabledProcessOptions, logService), undefined); + strictEqual(getShellIntegrationInjection({ executable: 'bash', args: undefined }, disabledProcessOptions, logService), undefined); }); test('when custom array entry', () => { - strictEqual(getShellIntegrationInjection({ executable: 'bash', args: ['-l', '-i'] }, disabledProcessOptions), undefined); + strictEqual(getShellIntegrationInjection({ executable: 'bash', args: ['-l', '-i'] }, disabledProcessOptions, logService), undefined); }); }); }); diff --git a/src/vs/platform/theme/common/colorRegistry.ts b/src/vs/platform/theme/common/colorRegistry.ts index caf8e6786d8..6e163e27d16 100644 --- a/src/vs/platform/theme/common/colorRegistry.ts +++ b/src/vs/platform/theme/common/colorRegistry.ts @@ -274,11 +274,11 @@ export const buttonSecondaryForeground = registerColor('button.secondaryForegrou export const buttonSecondaryBackground = registerColor('button.secondaryBackground', { dark: '#3A3D41', light: '#5F6A79', hcDark: null, hcLight: Color.white }, nls.localize('buttonSecondaryBackground', "Secondary button background color.")); export const buttonSecondaryHoverBackground = registerColor('button.secondaryHoverBackground', { dark: lighten(buttonSecondaryBackground, 0.2), light: darken(buttonSecondaryBackground, 0.2), hcDark: null, hcLight: null }, nls.localize('buttonSecondaryHoverBackground', "Secondary button background color when hovering.")); -export const badgeBackground = registerColor('badge.background', { dark: '#4D4D4D', light: '#C4C4C4', hcDark: Color.black, hcLight: '#007ACC' }, nls.localize('badgeBackground', "Badge background color. Badges are small information labels, e.g. for search results count.")); +export const badgeBackground = registerColor('badge.background', { dark: '#4D4D4D', light: '#C4C4C4', hcDark: Color.black, hcLight: '#0F4A85' }, nls.localize('badgeBackground', "Badge background color. Badges are small information labels, e.g. for search results count.")); export const badgeForeground = registerColor('badge.foreground', { dark: Color.white, light: '#333', hcDark: Color.white, hcLight: Color.white }, nls.localize('badgeForeground', "Badge foreground color. Badges are small information labels, e.g. for search results count.")); export const scrollbarShadow = registerColor('scrollbar.shadow', { dark: '#000000', light: '#DDDDDD', hcDark: null, hcLight: null }, nls.localize('scrollbarShadow', "Scrollbar shadow to indicate that the view is scrolled.")); -export const scrollbarSliderBackground = registerColor('scrollbarSlider.background', { dark: Color.fromHex('#797979').transparent(0.4), light: Color.fromHex('#646464').transparent(0.4), hcDark: transparent(contrastBorder, 0.6), hcLight: transparent(contrastBorder, 0.6) }, nls.localize('scrollbarSliderBackground', "Scrollbar slider background color.")); +export const scrollbarSliderBackground = registerColor('scrollbarSlider.background', { dark: Color.fromHex('#797979').transparent(0.4), light: Color.fromHex('#646464').transparent(0.4), hcDark: transparent(contrastBorder, 0.6), hcLight: transparent(contrastBorder, 0.4) }, nls.localize('scrollbarSliderBackground', "Scrollbar slider background color.")); export const scrollbarSliderHoverBackground = registerColor('scrollbarSlider.hoverBackground', { dark: Color.fromHex('#646464').transparent(0.7), light: Color.fromHex('#646464').transparent(0.7), hcDark: transparent(contrastBorder, 0.8), hcLight: transparent(contrastBorder, 0.8) }, nls.localize('scrollbarSliderHoverBackground', "Scrollbar slider background color when hovering.")); export const scrollbarSliderActiveBackground = registerColor('scrollbarSlider.activeBackground', { dark: Color.fromHex('#BFBFBF').transparent(0.4), light: Color.fromHex('#000000').transparent(0.6), hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('scrollbarSliderActiveBackground', "Scrollbar slider background color when clicked on.")); @@ -427,15 +427,15 @@ export const diffDiagonalFill = registerColor('diffEditor.diagonalFill', { dark: export const listFocusBackground = registerColor('list.focusBackground', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('listFocusBackground', "List/Tree background color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); export const listFocusForeground = registerColor('list.focusForeground', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('listFocusForeground', "List/Tree foreground color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); export const listFocusOutline = registerColor('list.focusOutline', { dark: focusBorder, light: focusBorder, hcDark: activeContrastBorder, hcLight: activeContrastBorder }, nls.localize('listFocusOutline', "List/Tree outline color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); -export const listActiveSelectionBackground = registerColor('list.activeSelectionBackground', { dark: '#094771', light: '#0060C0', hcDark: null, hcLight: null }, nls.localize('listActiveSelectionBackground', "List/Tree background color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); +export const listActiveSelectionBackground = registerColor('list.activeSelectionBackground', { dark: '#094771', light: '#0060C0', hcDark: null, hcLight: Color.fromHex('#0F4A85').transparent(0.1) }, nls.localize('listActiveSelectionBackground', "List/Tree background color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); export const listActiveSelectionForeground = registerColor('list.activeSelectionForeground', { dark: Color.white, light: Color.white, hcDark: null, hcLight: null }, nls.localize('listActiveSelectionForeground', "List/Tree foreground color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); export const listActiveSelectionIconForeground = registerColor('list.activeSelectionIconForeground', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('listActiveSelectionIconForeground', "List/Tree icon foreground color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); -export const listInactiveSelectionBackground = registerColor('list.inactiveSelectionBackground', { dark: '#37373D', light: '#E4E6F1', hcDark: null, hcLight: null }, nls.localize('listInactiveSelectionBackground', "List/Tree background color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); +export const listInactiveSelectionBackground = registerColor('list.inactiveSelectionBackground', { dark: '#37373D', light: '#E4E6F1', hcDark: null, hcLight: Color.fromHex('#0F4A85').transparent(0.1) }, nls.localize('listInactiveSelectionBackground', "List/Tree background color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); export const listInactiveSelectionForeground = registerColor('list.inactiveSelectionForeground', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('listInactiveSelectionForeground', "List/Tree foreground color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); export const listInactiveSelectionIconForeground = registerColor('list.inactiveSelectionIconForeground', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('listInactiveSelectionIconForeground', "List/Tree icon foreground color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); export const listInactiveFocusBackground = registerColor('list.inactiveFocusBackground', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('listInactiveFocusBackground', "List/Tree background color for the focused item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); export const listInactiveFocusOutline = registerColor('list.inactiveFocusOutline', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('listInactiveFocusOutline', "List/Tree outline color for the focused item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); -export const listHoverBackground = registerColor('list.hoverBackground', { dark: '#2A2D2E', light: '#F0F0F0', hcDark: null, hcLight: null }, nls.localize('listHoverBackground', "List/Tree background when hovering over items using the mouse.")); +export const listHoverBackground = registerColor('list.hoverBackground', { dark: '#2A2D2E', light: '#F0F0F0', hcDark: null, hcLight: Color.fromHex('#0F4A85').transparent(0.1) }, nls.localize('listHoverBackground', "List/Tree background when hovering over items using the mouse.")); export const listHoverForeground = registerColor('list.hoverForeground', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('listHoverForeground', "List/Tree foreground when hovering over items using the mouse.")); export const listDropBackground = registerColor('list.dropBackground', { dark: '#062F4A', light: '#D6EBFF', hcDark: null, hcLight: null }, nls.localize('listDropBackground', "List/Tree drag and drop background when moving items around using the mouse.")); export const listHighlightForeground = registerColor('list.highlightForeground', { dark: '#18A3FF', light: '#0066BF', hcDark: focusBorder, hcLight: focusBorder }, nls.localize('highlight', 'List/Tree foreground color of the match highlights when searching inside the list/tree.')); @@ -447,7 +447,7 @@ export const listFilterWidgetBackground = registerColor('listFilterWidget.backgr export const listFilterWidgetOutline = registerColor('listFilterWidget.outline', { dark: Color.transparent, light: Color.transparent, hcDark: '#f38518', hcLight: '#007ACC' }, nls.localize('listFilterWidgetOutline', 'Outline color of the type filter widget in lists and trees.')); export const listFilterWidgetNoMatchesOutline = registerColor('listFilterWidget.noMatchesOutline', { dark: '#BE1100', light: '#BE1100', hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('listFilterWidgetNoMatchesOutline', 'Outline color of the type filter widget in lists and trees, when there are no matches.')); export const listFilterMatchHighlight = registerColor('list.filterMatchBackground', { dark: editorFindMatchHighlight, light: editorFindMatchHighlight, hcDark: null, hcLight: null }, nls.localize('listFilterMatchHighlight', 'Background color of the filtered match.')); -export const listFilterMatchHighlightBorder = registerColor('list.filterMatchBorder', { dark: editorFindMatchHighlightBorder, light: editorFindMatchHighlightBorder, hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('listFilterMatchHighlightBorder', 'Border color of the filtered match.')); +export const listFilterMatchHighlightBorder = registerColor('list.filterMatchBorder', { dark: editorFindMatchHighlightBorder, light: editorFindMatchHighlightBorder, hcDark: contrastBorder, hcLight: activeContrastBorder }, nls.localize('listFilterMatchHighlightBorder', 'Border color of the filtered match.')); export const treeIndentGuidesStroke = registerColor('tree.indentGuidesStroke', { dark: '#585858', light: '#a9a9a9', hcDark: '#a9a9a9', hcLight: '#a5a5a5' }, nls.localize('treeIndentGuidesStroke', "Tree stroke color for the indentation guides.")); export const tableColumnsBorder = registerColor('tree.tableColumnsBorder', { dark: '#CCCCCC20', light: '#61616120', hcDark: null, hcLight: null }, nls.localize('tableColumnsBorder', "Table border color between columns.")); export const tableOddRowsBackgroundColor = registerColor('tree.tableOddRowsBackground', { dark: transparent(foreground, 0.04), light: transparent(foreground, 0.04), hcDark: null, hcLight: null }, nls.localize('tableOddRowsBackgroundColor', "Background color for odd table rows.")); @@ -493,7 +493,7 @@ export const snippetFinalTabstopHighlightBorder = registerColor('editor.snippetF export const breadcrumbsForeground = registerColor('breadcrumb.foreground', { light: transparent(foreground, 0.8), dark: transparent(foreground, 0.8), hcDark: transparent(foreground, 0.8), hcLight: transparent(foreground, 0.8) }, nls.localize('breadcrumbsFocusForeground', "Color of focused breadcrumb items.")); export const breadcrumbsBackground = registerColor('breadcrumb.background', { light: editorBackground, dark: editorBackground, hcDark: editorBackground, hcLight: editorBackground }, nls.localize('breadcrumbsBackground', "Background color of breadcrumb items.")); export const breadcrumbsFocusForeground = registerColor('breadcrumb.focusForeground', { light: darken(foreground, 0.2), dark: lighten(foreground, 0.1), hcDark: lighten(foreground, 0.1), hcLight: lighten(foreground, 0.1) }, nls.localize('breadcrumbsFocusForeground', "Color of focused breadcrumb items.")); -export const breadcrumbsActiveSelectionForeground = registerColor('breadcrumb.activeSelectionForeground', { light: darken(foreground, 0.2), dark: lighten(foreground, 0.1), hcDark: lighten(foreground, 0.1), hcLight: lighten(foreground, 0.1) }, nls.localize('breadcrumbsSelectedForegound', "Color of selected breadcrumb items.")); +export const breadcrumbsActiveSelectionForeground = registerColor('breadcrumb.activeSelectionForeground', { light: darken(foreground, 0.2), dark: lighten(foreground, 0.1), hcDark: lighten(foreground, 0.1), hcLight: lighten(foreground, 0.1) }, nls.localize('breadcrumbsSelectedForeground', "Color of selected breadcrumb items.")); export const breadcrumbsPickerBackground = registerColor('breadcrumbPicker.background', { light: editorWidgetBackground, dark: editorWidgetBackground, hcDark: editorWidgetBackground, hcLight: editorWidgetBackground }, nls.localize('breadcrumbsSelectedBackground', "Background color of breadcrumb item picker.")); /** diff --git a/src/vs/platform/update/electron-main/abstractUpdateService.ts b/src/vs/platform/update/electron-main/abstractUpdateService.ts index 5adf20c74e4..86737bd1657 100644 --- a/src/vs/platform/update/electron-main/abstractUpdateService.ts +++ b/src/vs/platform/update/electron-main/abstractUpdateService.ts @@ -57,7 +57,7 @@ export abstract class AbstractUpdateService implements IUpdateService { * optimization, to avoid using extra CPU cycles before first window open. * https://github.com/microsoft/vscode/issues/89784 */ - initialize(): void { + async initialize(): Promise<void> { if (!this.environmentMainService.isBuilt) { return; // updates are never enabled when running out of sources } @@ -72,7 +72,7 @@ export abstract class AbstractUpdateService implements IUpdateService { return; } - const updateMode = this.getUpdateMode(); + const updateMode = await this.getUpdateMode(); const quality = this.getProductQuality(updateMode); if (!quality) { @@ -104,7 +104,7 @@ export abstract class AbstractUpdateService implements IUpdateService { } } - private getUpdateMode(): 'none' | 'manual' | 'start' | 'default' { + protected async getUpdateMode(): Promise<'none' | 'manual' | 'start' | 'default'> { return getMigratedSettingValue<'none' | 'manual' | 'start' | 'default'>(this.configurationService, 'update.mode', 'update.channel'); } @@ -184,7 +184,11 @@ export abstract class AbstractUpdateService implements IUpdateService { async isLatestVersion(): Promise<boolean | undefined> { if (!this.url) { return undefined; - } else if (this.getUpdateMode() === 'none') { + } + + const mode = await this.getUpdateMode(); + + if (mode === 'none') { return false; } diff --git a/src/vs/platform/update/electron-main/updateService.darwin.ts b/src/vs/platform/update/electron-main/updateService.darwin.ts index 0c008ea8504..688ea356ef1 100644 --- a/src/vs/platform/update/electron-main/updateService.darwin.ts +++ b/src/vs/platform/update/electron-main/updateService.darwin.ts @@ -38,8 +38,8 @@ export class DarwinUpdateService extends AbstractUpdateService { super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService); } - override initialize(): void { - super.initialize(); + override async initialize(): Promise<void> { + await super.initialize(); this.onRawError(this.onError, this, this.disposables); this.onRawUpdateAvailable(this.onUpdateAvailable, this, this.disposables); this.onRawUpdateDownloaded(this.onUpdateDownloaded, this, this.disposables); diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts index 781e766b7f7..e3661ce4bbc 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -47,6 +47,14 @@ function getUpdateType(): UpdateType { return _updateType; } +function validateUpdateModeValue(value: string | undefined): 'none' | 'manual' | 'start' | 'default' | undefined { + if (value === 'none' || value === 'manual' || value === 'start' || value === 'default') { + return value; + } else { + return undefined; + } +} + export class Win32UpdateService extends AbstractUpdateService { private availableUpdate: IAvailableUpdate | undefined; @@ -71,8 +79,24 @@ export class Win32UpdateService extends AbstractUpdateService { super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService); } - override initialize(): void { - super.initialize(); + protected override async getUpdateMode(): Promise<'none' | 'manual' | 'start' | 'default'> { + if (this.productService.win32RegValueName) { + const policyKey = `Software\\Policies\\Microsoft\\${this.productService.win32RegValueName}`; + const [hklm, hkcu] = await Promise.all([ + this.nativeHostMainService.windowsGetStringRegKey(undefined, 'HKEY_LOCAL_MACHINE', policyKey, 'UpdateMode').then(validateUpdateModeValue), + this.nativeHostMainService.windowsGetStringRegKey(undefined, 'HKEY_CURRENT_USER', policyKey, 'UpdateMode').then(validateUpdateModeValue) + ]); + + if (hklm) { + this.logService.info(`update#getUpdateMode: 'UpdateMode' policy defined in 'HKLM\\${policyKey}':`, hklm); + return hklm; + } else if (hkcu) { + this.logService.info(`update#getUpdateMode: 'UpdateMode' policy defined in 'HKCU\\${policyKey}':`, hkcu); + return hkcu; + } + } + + return await super.getUpdateMode(); } protected buildUpdateFeedUrl(quality: string): string | undefined { diff --git a/src/vs/platform/userDataSync/common/extensionsSync.ts b/src/vs/platform/userDataSync/common/extensionsSync.ts index d3411d42879..2aae1a5028b 100644 --- a/src/vs/platform/userDataSync/common/extensionsSync.ts +++ b/src/vs/platform/userDataSync/common/extensionsSync.ts @@ -115,7 +115,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse const skippedExtensions: ISyncExtension[] = lastSyncUserData?.skippedExtensions || []; const lastSyncExtensions: ISyncExtension[] | null = lastSyncUserData?.syncData ? await parseAndMigrateExtensions(lastSyncUserData.syncData, this.extensionManagementService) : null; - const installedExtensions = await this.extensionManagementService.getInstalled(undefined, true); + const installedExtensions = await this.extensionManagementService.getInstalled(undefined); const localExtensions = this.getLocalExtensions(installedExtensions); const ignoredExtensions = this.ignoredExtensionsManagementService.getIgnoredExtensions(installedExtensions); @@ -150,7 +150,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse protected async hasRemoteChanged(lastSyncUserData: ILastSyncUserData): Promise<boolean> { const lastSyncExtensions: ISyncExtension[] | null = lastSyncUserData.syncData ? await parseAndMigrateExtensions(lastSyncUserData.syncData, this.extensionManagementService) : null; - const installedExtensions = await this.extensionManagementService.getInstalled(undefined, true); + const installedExtensions = await this.extensionManagementService.getInstalled(undefined); const localExtensions = this.getLocalExtensions(installedExtensions); const ignoredExtensions = this.ignoredExtensionsManagementService.getIgnoredExtensions(installedExtensions); const { remote } = merge(localExtensions, lastSyncExtensions, lastSyncExtensions, lastSyncUserData.skippedExtensions || [], ignoredExtensions); diff --git a/src/vs/platform/userDataSync/common/settingsMerge.ts b/src/vs/platform/userDataSync/common/settingsMerge.ts index 3891690df00..b34256a1e48 100644 --- a/src/vs/platform/userDataSync/common/settingsMerge.ts +++ b/src/vs/platform/userDataSync/common/settingsMerge.ts @@ -57,6 +57,16 @@ function getIgnoredSettingsFromContent(settingsContent: string): string[] { return parsed ? parsed['settingsSync.ignoredSettings'] || parsed['sync.ignoredSettings'] || [] : []; } +export function removeComments(content: string, formattingOptions: FormattingOptions): string { + const source = parse(content) || {}; + let result = '{}'; + for (const key of Object.keys(source)) { + const edits = setProperty(result, [key], source[key], formattingOptions); + result = applyEdits(result, edits); + } + return result; +} + export function updateIgnoredSettings(targetContent: string, sourceContent: string, ignoredSettings: string[], formattingOptions: FormattingOptions): string { if (ignoredSettings.length) { const sourceTree = parseSettings(sourceContent); diff --git a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts index f498de36c89..8ae695baae4 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts @@ -20,7 +20,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IFileService } from 'vs/platform/files/common/files'; import { IProductService } from 'vs/platform/product/common/productService'; -import { asJson, asText, IRequestService, isSuccess as isSuccessContext } from 'vs/platform/request/common/request'; +import { asJson, asTextOrError, IRequestService, isSuccess as isSuccessContext } from 'vs/platform/request/common/request'; import { getServiceMachineId } from 'vs/platform/externalServices/common/serviceMachineId'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { CONFIGURATION_SYNC_STORE_KEY, HEADER_EXECUTION_ID, HEADER_OPERATION_ID, IAuthenticationProvider, IResourceRefHandle, IUserData, IUserDataManifest, IUserDataSyncLogService, IUserDataSyncStore, IUserDataSyncStoreClient, IUserDataSyncStoreManagementService, IUserDataSyncStoreService, ServerResource, SYNC_SERVICE_URL_TYPE, UserDataSyncErrorCode, UserDataSyncStoreError, UserDataSyncStoreType } from 'vs/platform/userDataSync/common/userDataSync'; @@ -254,7 +254,7 @@ export class UserDataSyncStoreClient extends Disposable implements IUserDataSync headers['Cache-Control'] = 'no-cache'; const context = await this.request(url, { type: 'GET', headers }, [], CancellationToken.None); - const content = await asText(context); + const content = await asTextOrError(context); return content; } @@ -295,7 +295,7 @@ export class UserDataSyncStoreClient extends Disposable implements IUserDataSync throw new UserDataSyncStoreError('Server did not return the ref', url, UserDataSyncErrorCode.NoRef, context.res.statusCode, context.res.headers[HEADER_OPERATION_ID]); } - const content = await asText(context); + const content = await asTextOrError(context); if (!content && context.res.statusCode === 304) { throw new UserDataSyncStoreError('Empty response', url, UserDataSyncErrorCode.EmptyResponse, context.res.statusCode, context.res.headers[HEADER_OPERATION_ID]); } @@ -352,7 +352,7 @@ export class UserDataSyncStoreClient extends Disposable implements IUserDataSync throw new UserDataSyncStoreError('Server did not return the ref', url, UserDataSyncErrorCode.NoRef, context.res.statusCode, context.res.headers[HEADER_OPERATION_ID]); } - const content = await asText(context); + const content = await asTextOrError(context); if (!content && context.res.statusCode === 304) { throw new UserDataSyncStoreError('Empty response', url, UserDataSyncErrorCode.EmptyResponse, context.res.statusCode, context.res.headers[HEADER_OPERATION_ID]); } diff --git a/src/vs/platform/window/common/window.ts b/src/vs/platform/window/common/window.ts index 9fd60d23a45..2c9591a2bec 100644 --- a/src/vs/platform/window/common/window.ts +++ b/src/vs/platform/window/common/window.ts @@ -8,6 +8,7 @@ import { isLinux, isMacintosh, isNative, isWeb } from 'vs/base/common/platform'; import { URI, UriComponents } from 'vs/base/common/uri'; import { ISandboxConfiguration } from 'vs/base/parts/sandbox/common/sandboxTypes'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { FileType } from 'vs/platform/files/common/files'; import { LogLevel } from 'vs/platform/log/common/log'; @@ -167,43 +168,44 @@ export function getTitleBarStyle(configurationService: IConfigurationService): ' return isLinux ? 'native' : 'custom'; // default to custom on all macOS and Windows } -export interface IPath extends IPathData { +export interface IPath<T = IEditorOptions> extends IPathData<T> { - // the file path to open within the instance + /** + * The file path to open within the instance + */ fileUri?: URI; } -export interface IPathData { +export interface IPathData<T = IEditorOptions> { - // the file path to open within the instance + /** + * The file path to open within the instance + */ readonly fileUri?: UriComponents; /** - * An optional selection to apply in the file + * Optional editor options to apply in the file + */ + readonly options?: T; + + /** + * A hint that the file exists. if true, the + * file exists, if false it does not. with + * `undefined` the state is unknown. */ - readonly selection?: { - readonly startLineNumber: number; - readonly startColumn: number; - readonly endLineNumber?: number; - readonly endColumn?: number; - }; - - // a hint that the file exists. if true, the - // file exists, if false it does not. with - // `undefined` the state is unknown. readonly exists?: boolean; - // a hint about the file type of this path. - // with `undefined` the type is unknown. + /** + * A hint about the file type of this path. + * with `undefined` the type is unknown. + */ readonly type?: FileType; - // Specifies if the file should be only be opened - // if it exists + /** + * Specifies if the file should be only be opened + * if it exists. + */ readonly openOnlyIfExists?: boolean; - - // Specifies an optional id to override the editor - // used to edit the resource, e.g. custom editor. - readonly editorOverrideId?: string; } export interface IPathsToWaitFor extends IPathsToWaitForData { diff --git a/src/vs/platform/windows/electron-main/window.ts b/src/vs/platform/windows/electron-main/window.ts index b6fe6d5f906..592f26956b1 100644 --- a/src/vs/platform/windows/electron-main/window.ts +++ b/src/vs/platform/windows/electron-main/window.ts @@ -29,7 +29,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; import { IProtocolMainService } from 'vs/platform/protocol/electron-main/protocol'; import { resolveMarketplaceHeaders } from 'vs/platform/externalServices/common/marketplace'; -import { IGlobalStorageMainService } from 'vs/platform/storage/electron-main/storageMainService'; +import { IGlobalStorageMainService, IStorageMainService } from 'vs/platform/storage/electron-main/storageMainService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { IThemeMainService } from 'vs/platform/theme/electron-main/themeMainService'; @@ -38,6 +38,7 @@ import { IWindowsMainService, OpenContext } from 'vs/platform/windows/electron-m import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; import { IWorkspacesManagementMainService } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService'; import { IWindowState, ICodeWindow, ILoadEvent, WindowMode, WindowError, LoadReason, defaultWindowState } from 'vs/platform/window/electron-main/window'; +import { Color } from 'vs/base/common/color'; export interface IWindowCreationOptions { state: IWindowState; @@ -106,23 +107,23 @@ export class CodeWindow extends Disposable implements ICodeWindow { private _lastFocusTime = -1; get lastFocusTime(): number { return this._lastFocusTime; } - get backupPath(): string | undefined { return this.currentConfig?.backupPath; } + get backupPath(): string | undefined { return this._config?.backupPath; } - get openedWorkspace(): IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | undefined { return this.currentConfig?.workspace; } + get openedWorkspace(): IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | undefined { return this._config?.workspace; } - get remoteAuthority(): string | undefined { return this.currentConfig?.remoteAuthority; } + get remoteAuthority(): string | undefined { return this._config?.remoteAuthority; } - private currentConfig: INativeWindowConfiguration | undefined; - get config(): INativeWindowConfiguration | undefined { return this.currentConfig; } + private _config: INativeWindowConfiguration | undefined; + get config(): INativeWindowConfiguration | undefined { return this._config; } private hiddenTitleBarStyle: boolean | undefined; get hasHiddenTitleBarStyle(): boolean { return !!this.hiddenTitleBarStyle; } - get isExtensionDevelopmentHost(): boolean { return !!(this.currentConfig?.extensionDevelopmentPath); } + get isExtensionDevelopmentHost(): boolean { return !!(this._config?.extensionDevelopmentPath); } - get isExtensionTestHost(): boolean { return !!(this.currentConfig?.extensionTestsPath); } + get isExtensionTestHost(): boolean { return !!(this._config?.extensionTestsPath); } - get isExtensionDevelopmentTestFromCli(): boolean { return this.isExtensionDevelopmentHost && this.isExtensionTestHost && !this.currentConfig?.debugId; } + get isExtensionDevelopmentTestFromCli(): boolean { return this.isExtensionDevelopmentHost && this.isExtensionTestHost && !this._config?.debugId; } //#endregion @@ -150,6 +151,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService, @IFileService private readonly fileService: IFileService, @IGlobalStorageMainService private readonly globalStorageMainService: IGlobalStorageMainService, + @IStorageMainService private readonly storageMainService: IStorageMainService, @IConfigurationService private readonly configurationService: IConfigurationService, @IThemeMainService private readonly themeMainService: IThemeMainService, @IWorkspacesManagementMainService private readonly workspacesManagementMainService: IWorkspacesManagementMainService, @@ -245,6 +247,19 @@ export class CodeWindow extends Disposable implements ICodeWindow { if (!isMacintosh) { options.frame = false; } + + if (isWindows) { + // This logic will not perfectly guess the right colors to use on initialization, + // but prefer to keep things simple as it is temporary and not noticeable + const titleBarColor = this.themeMainService.getWindowSplash()?.colorInfo.titleBarBackground ?? this.themeMainService.getBackgroundColor(); + const symbolColor = Color.fromHex(titleBarColor).isDarker() ? '#FFFFFF' : '#000000'; + + options.titleBarOverlay = { + height: 29, // The smallest size of the title bar on windows accounting for the border on windows 11 + color: titleBarColor, + symbolColor + }; + } } // Create the browser window @@ -368,7 +383,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { private readyState = ReadyState.NONE; setReady(): void { - this.logService.info(`window#load: window reported ready (id: ${this._id})`); + this.logService.trace(`window#load: window reported ready (id: ${this._id})`); this.readyState = ReadyState.READY; @@ -439,7 +454,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { // Associate properties from the load request if provided if (this.pendingLoadConfig) { - this.currentConfig = this.pendingLoadConfig; + this._config = this.pendingLoadConfig; this.pendingLoadConfig = undefined; } @@ -452,16 +467,16 @@ export class CodeWindow extends Disposable implements ICodeWindow { // Window (Un)Maximize this._win.on('maximize', (e: Event) => { - if (this.currentConfig) { - this.currentConfig.maximized = true; + if (this._config) { + this._config.maximized = true; } app.emit('browser-window-maximize', e, this._win); }); this._win.on('unmaximize', (e: Event) => { - if (this.currentConfig) { - this.currentConfig.maximized = false; + if (this._config) { + this._config.maximized = false; } app.emit('browser-window-unmaximize', e, this._win); @@ -496,6 +511,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { if (!this.marketplaceHeadersPromise) { this.marketplaceHeadersPromise = resolveMarketplaceHeaders(this.productService.version, this.productService, this.environmentMainService, this.configurationService, this.fileService, this.globalStorageMainService); } + return this.marketplaceHeadersPromise; } @@ -518,9 +534,11 @@ export class CodeWindow extends Disposable implements ICodeWindow { // Telemetry type WindowErrorClassification = { - type: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; owner: 'bpasero'; comment: 'The type of window crash to understand the nature of the crash better.' }; - reason: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; owner: 'bpasero'; comment: 'The reason of the window crash to understand the nature of the crash better.' }; - code: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; owner: 'bpasero'; comment: 'The exit code of the window process to understand the nature of the crash better' }; + type: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'The type of window crash to understand the nature of the crash better.' }; + reason: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'The reason of the window crash to understand the nature of the crash better.' }; + code: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; omment: 'The exit code of the window process to understand the nature of the crash better' }; + owner: 'bpasero'; + comment: 'Provides insight into reasons the vscode window crashes.'; }; type WindowErrorEvent = { type: WindowError; @@ -543,8 +561,8 @@ export class CodeWindow extends Disposable implements ICodeWindow { } // If we run smoke tests, we never want to show a blocking dialog - if (this.environmentMainService.driverHandle) { - this.destroyWindow(false); + if (this.environmentMainService.args['enable-smoke-test-driver']) { + this.destroyWindow(false, false); return; } @@ -573,13 +591,14 @@ export class CodeWindow extends Disposable implements ICodeWindow { detail: localize('appStalledDetail', "You can reopen or close the window or keep waiting."), noLink: true, defaultId: 0, - cancelId: 1 + cancelId: 1, + checkboxLabel: this._config?.workspace ? localize('doNotRestoreEditors', "Don't restore editors") : undefined }, this._win); // Handle choice if (result.response !== 1 /* keep waiting */) { const reopen = result.response === 0; - this.destroyWindow(reopen); + this.destroyWindow(reopen, result.checkboxChecked); } } @@ -603,18 +622,32 @@ export class CodeWindow extends Disposable implements ICodeWindow { message, detail: localize('appCrashedDetail', "We are sorry for the inconvenience. You can reopen the window to continue where you left off."), noLink: true, - defaultId: 0 + defaultId: 0, + checkboxLabel: this._config?.workspace ? localize('doNotRestoreEditors', "Don't restore editors") : undefined }, this._win); // Handle choice const reopen = result.response === 0; - this.destroyWindow(reopen); + this.destroyWindow(reopen, result.checkboxChecked); } break; } } - private destroyWindow(reopen: boolean): void { + private async destroyWindow(reopen: boolean, skipRestoreEditors: boolean): Promise<void> { + const workspace = this._config?.workspace; + + // check to discard editor state first + if (skipRestoreEditors && workspace) { + try { + const workspaceStorage = this.storageMainService.workspaceStorage(workspace); + await workspaceStorage.init(); + workspaceStorage.delete('memento/workbench.parts.editor'); + await workspaceStorage.close(); + } catch (error) { + this.logService.error(error); + } + } // 'close' event will not be fired on destroy(), so signal crash via explicit event this._onDidDestroy.fire(); @@ -623,15 +656,15 @@ export class CodeWindow extends Disposable implements ICodeWindow { this._win?.destroy(); // ask the windows service to open a new fresh window if specified - if (reopen && this.config) { + if (reopen && this._config) { // We have to reconstruct a openable from the current workspace - let workspace: IWorkspaceToOpen | IFolderToOpen | undefined = undefined; + let uriToOpen: IWorkspaceToOpen | IFolderToOpen | undefined = undefined; let forceEmpty = undefined; - if (isSingleFolderWorkspaceIdentifier(this.openedWorkspace)) { - workspace = { folderUri: this.openedWorkspace.uri }; - } else if (isWorkspaceIdentifier(this.openedWorkspace)) { - workspace = { workspaceUri: this.openedWorkspace.configPath }; + if (isSingleFolderWorkspaceIdentifier(workspace)) { + uriToOpen = { folderUri: workspace.uri }; + } else if (isWorkspaceIdentifier(workspace)) { + uriToOpen = { workspaceUri: workspace.configPath }; } else { forceEmpty = true; } @@ -639,12 +672,12 @@ export class CodeWindow extends Disposable implements ICodeWindow { // Delegate to windows service const [window] = this.windowsMainService.open({ context: OpenContext.API, - userEnv: this.config.userEnv, + userEnv: this._config.userEnv, cli: { ...this.environmentMainService.args, _: [] // we pass in the workspace to open explicitly via `urisToOpen` }, - urisToOpen: workspace ? [workspace] : undefined, + urisToOpen: uriToOpen ? [uriToOpen] : undefined, forceEmpty, forceNewWindow: true, remoteAuthority: this.remoteAuthority @@ -657,8 +690,8 @@ export class CodeWindow extends Disposable implements ICodeWindow { // Make sure to update our workspace config if we detect that it // was deleted - if (this.openedWorkspace?.id === workspace.id && this.currentConfig) { - this.currentConfig.workspace = undefined; + if (this._config?.workspace?.id === workspace.id && this._config) { + this._config.workspace = undefined; } } @@ -699,7 +732,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { } load(configuration: INativeWindowConfiguration, options: ILoadOptions = Object.create(null)): void { - this.logService.info(`window#load: attempt to load window (id: ${this._id})`); + this.logService.trace(`window#load: attempt to load window (id: ${this._id})`); // Clear Document Edited if needed if (this.isDocumentEdited()) { @@ -724,7 +757,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { // If this is the first time the window is loaded, we associate the paths // directly with the window because we assume the loading will just work if (this.readyState === ReadyState.NONE) { - this.currentConfig = configuration; + this._config = configuration; } // Otherwise, the window is currently showing a folder and if there is an @@ -775,7 +808,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { // Also, preserve the environment if we're loading from an // extension development host that had its environment set // (for https://github.com/microsoft/vscode/issues/123508) - const currentUserEnv = (this.currentConfig ?? this.pendingLoadConfig)?.userEnv; + const currentUserEnv = (this._config ?? this.pendingLoadConfig)?.userEnv; if (currentUserEnv) { const shouldPreserveLaunchCliEnvironment = isLaunchedFromCli(currentUserEnv) && !isLaunchedFromCli(configuration.userEnv); const shouldPreserveDebugEnvironmnet = this.isExtensionDevelopmentHost; @@ -815,7 +848,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { async reload(cli?: NativeParsedArgs): Promise<void> { // Copy our current config for reuse - const configuration = Object.assign({}, this.currentConfig); + const configuration = Object.assign({}, this._config); // Validate workspace configuration.workspace = await this.validateWorkspaceBeforeReload(configuration); diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index 1dbfaaf9df9..ec6d1848956 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -17,7 +17,7 @@ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { basename, join, normalize, posix } from 'vs/base/common/path'; import { getMarks, mark } from 'vs/base/common/performance'; -import { IProcessEnvironment, isMacintosh, isWindows } from 'vs/base/common/platform'; +import { IProcessEnvironment, isMacintosh, isWindows, OS } from 'vs/base/common/platform'; import { cwd } from 'vs/base/common/process'; import { extUriBiasedIgnorePathCase, isEqualAuthority, normalizePath, originalFSPath, removeTrailingPathSeparator } from 'vs/base/common/resources'; import { assertIsDefined, withNullAsUndefined } from 'vs/base/common/types'; @@ -50,6 +50,7 @@ import { IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-m import { IWorkspacesManagementMainService } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService'; import { ICodeWindow, UnloadReason } from 'vs/platform/window/electron-main/window'; import { IThemeMainService } from 'vs/platform/theme/electron-main/themeMainService'; +import { IEditorOptions, ITextEditorOptions } from 'vs/platform/editor/common/editor'; //#region Helper Interfaces @@ -118,23 +119,33 @@ interface IFilesToOpen { filesToWait?: IPathsToWaitFor; } -interface IPathToOpen extends IPath { +interface IPathToOpen<T = IEditorOptions> extends IPath<T> { - // the workspace to open + /** + * The workspace to open + */ readonly workspace?: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier; - // whether the path is considered to be transient or not - // for example, a transient workspace should not add to - // the workspaces history and should never restore + /** + * Whether the path is considered to be transient or not + * for example, a transient workspace should not add to + * the workspaces history and should never restore. + */ readonly transient?: boolean; - // the backup path to use + /** + * The backup path to use + */ readonly backupPath?: string; - // the remote authority for the Code instance to open. Undefined if not remote. + /** + * The remote authority for the Code instance to open. Undefined if not remote. + */ readonly remoteAuthority?: string; - // optional label for the recent history + /** + * Optional label for the recent history + */ label?: string; } @@ -756,8 +767,8 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic defaultId: 0, message: uri.scheme === Schemas.file ? localize('pathNotExistTitle', "Path does not exist") : localize('uriInvalidTitle', "URI can not be opened"), detail: uri.scheme === Schemas.file ? - localize('pathNotExistDetail', "The path '{0}' does not exist on this computer.", getPathLabel(uri.fsPath, this.environmentMainService)) : - localize('uriInvalidDetail', "The URI '{0}' is not valid and can not be opened.", uri.toString()), + localize('pathNotExistDetail', "The path '{0}' does not exist on this computer.", getPathLabel(uri, { os: OS, tildify: this.environmentMainService })) : + localize('uriInvalidDetail', "The URI '{0}' is not valid and can not be opened.", uri.toString(true)), noLink: true }; @@ -916,7 +927,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic return this.doResolveRemoteOpenable(openable, options); } - private doResolveRemoteOpenable(openable: IWindowOpenable, options: IPathResolveOptions): IPathToOpen | undefined { + private doResolveRemoteOpenable(openable: IWindowOpenable, options: IPathResolveOptions): IPathToOpen<ITextEditorOptions> | undefined { let uri = this.resourceFromOpenable(openable); // use remote authority from vscode @@ -932,7 +943,9 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic return { fileUri: uri.with({ path }), - selection: line ? { startLineNumber: line, startColumn: column || 1 } : undefined, + options: { + selection: line ? { startLineNumber: line, startColumn: column || 1 } : undefined + }, remoteAuthority }; } @@ -961,7 +974,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic return openable.fileUri; } - private doResolveFilePath(path: string, options: IPathResolveOptions): IPathToOpen | undefined { + private doResolveFilePath(path: string, options: IPathResolveOptions): IPathToOpen<ITextEditorOptions> | undefined { // Extract line/col information from path let lineNumber: number | undefined; @@ -1004,7 +1017,9 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic fileUri: URI.file(path), type: FileType.File, exists: true, - selection: lineNumber ? { startLineNumber: lineNumber, startColumn: columnNumber || 1 } : undefined + options: { + selection: lineNumber ? { startLineNumber: lineNumber, startColumn: columnNumber || 1 } : undefined + } }; } @@ -1047,7 +1062,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic return undefined; } - private doResolvePathRemote(path: string, options: IPathResolveOptions): IPathToOpen | undefined { + private doResolvePathRemote(path: string, options: IPathResolveOptions): IPathToOpen<ITextEditorOptions> | undefined { const first = path.charCodeAt(0); const remoteAuthority = options.remoteAuthority; @@ -1081,7 +1096,9 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic if (options.forceOpenWorkspaceAsFile) { return { fileUri: uri, - selection: lineNumber ? { startLineNumber: lineNumber, startColumn: columnNumber || 1 } : undefined, + options: { + selection: lineNumber ? { startLineNumber: lineNumber, startColumn: columnNumber || 1 } : undefined + }, remoteAuthority: options.remoteAuthority }; } @@ -1093,7 +1110,9 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic else if (options.gotoLineMode || posix.basename(path).indexOf('.') !== -1) { return { fileUri: uri, - selection: lineNumber ? { startLineNumber: lineNumber, startColumn: columnNumber || 1 } : undefined, + options: { + selection: lineNumber ? { startLineNumber: lineNumber, startColumn: columnNumber || 1 } : undefined + }, remoteAuthority }; } diff --git a/src/vs/platform/workspace/common/workspace.ts b/src/vs/platform/workspace/common/workspace.ts index 82c3edb83ea..555c522c741 100644 --- a/src/vs/platform/workspace/common/workspace.ts +++ b/src/vs/platform/workspace/common/workspace.ts @@ -6,7 +6,6 @@ import { localize } from 'vs/nls'; import { Event } from 'vs/base/common/event'; import { extname } from 'vs/base/common/path'; -import { IWorkspaceFolderProvider } from 'vs/base/common/labels'; import { TernarySearchTree } from 'vs/base/common/map'; import { extname as resourceExtname, basenameOrAuthority, joinPath, extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; import { URI, UriComponents } from 'vs/base/common/uri'; @@ -16,7 +15,7 @@ import { Schemas } from 'vs/base/common/network'; export const IWorkspaceContextService = createDecorator<IWorkspaceContextService>('contextService'); -export interface IWorkspaceContextService extends IWorkspaceFolderProvider { +export interface IWorkspaceContextService { readonly _serviceBrand: undefined; @@ -295,7 +294,7 @@ export function isWorkspaceFolder(thing: unknown): thing is IWorkspaceFolder { export class Workspace implements IWorkspace { - private _foldersMap: TernarySearchTree<URI, WorkspaceFolder> = TernarySearchTree.forUris<WorkspaceFolder>(this._ignorePathCasing); + private _foldersMap: TernarySearchTree<URI, WorkspaceFolder> = TernarySearchTree.forUris<WorkspaceFolder>(this._ignorePathCasing, () => true); private _folders!: WorkspaceFolder[]; constructor( @@ -346,15 +345,11 @@ export class Workspace implements IWorkspace { return null; } - return this._foldersMap.findSubstr(resource.with({ - scheme: resource.scheme, - authority: resource.authority, - path: resource.path - })) || null; + return this._foldersMap.findSubstr(resource) || null; } private updateFoldersMap(): void { - this._foldersMap = TernarySearchTree.forUris<WorkspaceFolder>(this._ignorePathCasing); + this._foldersMap = TernarySearchTree.forUris<WorkspaceFolder>(this._ignorePathCasing, () => true); for (const folder of this.folders) { this._foldersMap.set(folder.uri, folder); } diff --git a/src/vs/platform/workspaces/common/workspaces.ts b/src/vs/platform/workspaces/common/workspaces.ts index cb31aedbb25..e24c3311637 100644 --- a/src/vs/platform/workspaces/common/workspaces.ts +++ b/src/vs/platform/workspaces/common/workspaces.ts @@ -297,13 +297,6 @@ interface ISerializedRecentFile { readonly remoteAuthority?: string; } -interface ISerializedRecentlyOpenedLegacy { - readonly workspaces3: Array<{ id: string; configURIPath: string } | string>; // workspace or URI.toString() // added in 1.32 - readonly workspaceLabels?: Array<string | null>; // added in 1.33 - readonly files2: string[]; // files as URI.toString() // added in 1.32 - readonly fileLabels?: Array<string | null>; // added in 1.33 -} - interface ISerializedRecentlyOpened { readonly entries: Array<ISerializedRecentWorkspace | ISerializedRecentFolder | ISerializedRecentFile>; // since 1.55 } @@ -349,26 +342,6 @@ export function restoreRecentlyOpened(data: RecentlyOpenedStorageData | undefine result.files.push({ label, remoteAuthority, fileUri: URI.parse(entry.fileUri) }); } }); - } else { - const storedRecents2 = data as ISerializedRecentlyOpenedLegacy; - if (Array.isArray(storedRecents2.workspaces3)) { - restoreGracefully(storedRecents2.workspaces3, (workspace, i) => { - const label: string | undefined = (Array.isArray(storedRecents2.workspaceLabels) && storedRecents2.workspaceLabels[i]) || undefined; - if (typeof workspace === 'object' && typeof workspace.id === 'string' && typeof workspace.configURIPath === 'string') { - result.workspaces.push({ label, workspace: { id: workspace.id, configPath: URI.parse(workspace.configURIPath) } }); - } else if (typeof workspace === 'string') { - result.workspaces.push({ label, folderUri: URI.parse(workspace) }); - } - }); - } - if (Array.isArray(storedRecents2.files2)) { - restoreGracefully(storedRecents2.files2, (file, i) => { - const label: string | undefined = (Array.isArray(storedRecents2.fileLabels) && storedRecents2.fileLabels[i]) || undefined; - if (typeof file === 'string') { - result.files.push({ label, fileUri: URI.parse(file) }); - } - }); - } } } diff --git a/src/vs/platform/workspaces/test/electron-main/workspacesHistoryStorage.test.ts b/src/vs/platform/workspaces/test/electron-main/workspacesHistoryStorage.test.ts index a256666e45c..50a00b95314 100644 --- a/src/vs/platform/workspaces/test/electron-main/workspacesHistoryStorage.test.ts +++ b/src/vs/platform/workspaces/test/electron-main/workspacesHistoryStorage.test.ts @@ -107,39 +107,6 @@ suite('History Storage', () => { assertRestoring(ro, 'authority'); }); - test('open 1_33', () => { - const v1_33 = `{ - "workspaces3": [ - { - "id": "53b714b46ef1a2d4346568b4f591028c", - "configURIPath": "file:///home/user/workspaces/testing/custom.code-workspace" - }, - "file:///home/user/workspaces/testing/folding" - ], - "files2": [ - "file:///home/user/.config/code-oss-dev/storage.json" - ], - "workspaceLabels": [ - null, - "abc" - ], - "fileLabels": [ - "def" - ] - }`; - - let windowsState = restoreRecentlyOpened(JSON.parse(v1_33), new NullLogService()); - let expected: IRecentlyOpened = { - files: [{ label: 'def', fileUri: URI.parse('file:///home/user/.config/code-oss-dev/storage.json') }], - workspaces: [ - { workspace: { id: '53b714b46ef1a2d4346568b4f591028c', configPath: URI.parse('file:///home/user/workspaces/testing/custom.code-workspace') } }, - { label: 'abc', folderUri: URI.parse('file:///home/user/workspaces/testing/folding') } - ] - }; - - assertEqualRecentlyOpened(windowsState, expected, 'v1_33'); - }); - test('open 1_55', () => { const v1_55 = `{ "entries": [ diff --git a/src/vs/server/node/extensionHostConnection.ts b/src/vs/server/node/extensionHostConnection.ts index 4915eb0c283..96a972433da 100644 --- a/src/vs/server/node/extensionHostConnection.ts +++ b/src/vs/server/node/extensionHostConnection.ts @@ -19,7 +19,7 @@ import { IExtHostReadyMessage, IExtHostSocketMessage, IExtHostReduceGraceTimeMes import { IServerEnvironmentService } from 'vs/server/node/serverEnvironmentService'; import { IProcessEnvironment, isWindows } from 'vs/base/common/platform'; import { logRemoteEntry } from 'vs/workbench/services/extensions/common/remoteConsoleUtil'; -import { removeDangerousEnvVariables } from 'vs/base/node/processes'; +import { removeDangerousEnvVariables } from 'vs/base/common/processes'; import { IExtensionHostStatusService } from 'vs/server/node/extensionHostStatusService'; export async function buildUserEnvironment(startParamsEnv: { [key: string]: string | null } = {}, withUserShellEnvironment: boolean, language: string, isDebug: boolean, environmentService: IServerEnvironmentService, logService: ILogService): Promise<IProcessEnvironment> { diff --git a/src/vs/server/node/extensionsScannerService.ts b/src/vs/server/node/extensionsScannerService.ts new file mode 100644 index 00000000000..7f551bf5528 --- /dev/null +++ b/src/vs/server/node/extensionsScannerService.ts @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { joinPath } from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; +import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; +import { AbstractExtensionsScannerService, IExtensionsScannerService, Translations } from 'vs/platform/extensionManagement/common/extensionsScannerService'; +import { MANIFEST_CACHE_FOLDER } from 'vs/platform/extensions/common/extensions'; +import { IFileService } from 'vs/platform/files/common/files'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { getNLSConfiguration, InternalNLSConfiguration } from 'vs/server/node/remoteLanguagePacks'; + +export class ExtensionsScannerService extends AbstractExtensionsScannerService implements IExtensionsScannerService { + + constructor( + @IFileService fileService: IFileService, + @ILogService logService: ILogService, + @INativeEnvironmentService private readonly nativeEnvironmentService: INativeEnvironmentService, + @IProductService productService: IProductService, + ) { + super( + URI.file(nativeEnvironmentService.builtinExtensionsPath), + URI.file(nativeEnvironmentService.extensionsPath), + joinPath(nativeEnvironmentService.userHome, '.vscode-oss-dev', 'extensions', 'control.json'), + joinPath(URI.file(nativeEnvironmentService.userDataPath), MANIFEST_CACHE_FOLDER), + fileService, logService, nativeEnvironmentService, productService); + } + + protected async getTranslations(language: string): Promise<Translations> { + const config = await getNLSConfiguration(language, this.nativeEnvironmentService.userDataPath); + if (InternalNLSConfiguration.is(config)) { + try { + const content = await this.fileService.readFile(URI.file(config._translationsConfigFile)); + return JSON.parse(content.value.toString()); + } catch (err) { /* Ignore error */ } + } + return Object.create(null); + } + +} diff --git a/src/vs/server/node/remoteAgentEnvironmentImpl.ts b/src/vs/server/node/remoteAgentEnvironmentImpl.ts index 4b8502beb39..dd7ebc3daa6 100644 --- a/src/vs/server/node/remoteAgentEnvironmentImpl.ts +++ b/src/vs/server/node/remoteAgentEnvironmentImpl.ts @@ -10,43 +10,23 @@ import { URI } from 'vs/base/common/uri'; import { createURITransformer } from 'vs/workbench/api/node/uriTransformer'; import { IRemoteAgentEnvironmentDTO, IGetEnvironmentDataArguments, IScanExtensionsArguments, IScanSingleExtensionArguments, IGetExtensionHostExitInfoArguments } from 'vs/workbench/services/remote/common/remoteAgentEnvironmentChannel'; import * as nls from 'vs/nls'; -import { FileAccess, Schemas } from 'vs/base/common/network'; +import { Schemas } from 'vs/base/common/network'; import { IServerEnvironmentService } from 'vs/server/node/serverEnvironmentService'; -import { Translations, ExtensionScanner, ExtensionScannerInput, IExtensionResolver, IExtensionReference } from 'vs/workbench/services/extensions/common/extensionPoints'; import { IServerChannel } from 'vs/base/parts/ipc/common/ipc'; -import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, ExtensionType, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { transformOutgoingURIs } from 'vs/base/common/uriIpc'; import { ILogService } from 'vs/platform/log/common/log'; -import { getNLSConfiguration, InternalNLSConfiguration } from 'vs/server/node/remoteLanguagePacks'; import { ContextKeyExpr, ContextKeyDefinedExpr, ContextKeyNotExpr, ContextKeyEqualsExpr, ContextKeyNotEqualsExpr, ContextKeyRegexExpr, IContextKeyExprMapper, ContextKeyExpression, ContextKeyInExpr, ContextKeyGreaterExpr, ContextKeyGreaterEqualsExpr, ContextKeySmallerExpr, ContextKeySmallerEqualsExpr } from 'vs/platform/contextkey/common/contextkey'; import { listProcesses } from 'vs/base/node/ps'; import { getMachineInfo, collectWorkspaceStats } from 'vs/platform/diagnostics/node/diagnosticsService'; import { IDiagnosticInfoOptions, IDiagnosticInfo } from 'vs/platform/diagnostics/common/diagnostics'; -import { basename, isAbsolute, join, normalize } from 'vs/base/common/path'; +import { basename, isAbsolute, join, resolve } from 'vs/base/common/path'; import { ProcessItem } from 'vs/base/common/processes'; -import { IBuiltInExtension } from 'vs/base/common/product'; -import { IExtensionManagementCLIService, IExtensionManagementService, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionManagementCLIService, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; import { cwd } from 'vs/base/common/process'; -import * as pfs from 'vs/base/node/pfs'; -import { IProductService } from 'vs/platform/product/common/productService'; import { ServerConnectionToken, ServerConnectionTokenType } from 'vs/server/node/serverConnectionToken'; import { IExtensionHostStatusService } from 'vs/server/node/extensionHostStatusService'; -import { IFileService } from 'vs/platform/files/common/files'; - -let _SystemExtensionsRoot: string | null = null; -function getSystemExtensionsRoot(): string { - if (!_SystemExtensionsRoot) { - _SystemExtensionsRoot = normalize(join(FileAccess.asFileUri('', require).fsPath, '..', 'extensions')); - } - return _SystemExtensionsRoot; -} -let _ExtraDevSystemExtensionsRoot: string | null = null; -function getExtraDevSystemExtensionsRoot(): string { - if (!_ExtraDevSystemExtensionsRoot) { - _ExtraDevSystemExtensionsRoot = normalize(join(FileAccess.asFileUri('', require).fsPath, '..', '.build', 'builtInExtensions')); - } - return _ExtraDevSystemExtensionsRoot; -} +import { IExtensionsScannerService, toExtensionDescription } from 'vs/platform/extensionManagement/common/extensionsScannerService'; export class RemoteAgentEnvironmentChannel implements IServerChannel { @@ -56,31 +36,29 @@ export class RemoteAgentEnvironmentChannel implements IServerChannel { constructor( private readonly _connectionToken: ServerConnectionToken, - private readonly environmentService: IServerEnvironmentService, + private readonly _environmentService: IServerEnvironmentService, extensionManagementCLIService: IExtensionManagementCLIService, - private readonly _extensionManagementService: IExtensionManagementService, - private readonly logService: ILogService, - private readonly productService: IProductService, - private readonly extensionHostStatusService: IExtensionHostStatusService, - private readonly _fileService: IFileService, + private readonly _logService: ILogService, + private readonly _extensionHostStatusService: IExtensionHostStatusService, + private readonly _extensionsScannerService: IExtensionsScannerService, ) { - if (environmentService.args['install-builtin-extension']) { - const installOptions: InstallOptions = { isMachineScoped: !!environmentService.args['do-not-sync'], installPreReleaseVersion: !!environmentService.args['pre-release'] }; - this.whenExtensionsReady = extensionManagementCLIService.installExtensions([], environmentService.args['install-builtin-extension'], installOptions, !!environmentService.args['force']) + if (_environmentService.args['install-builtin-extension']) { + const installOptions: InstallOptions = { isMachineScoped: !!_environmentService.args['do-not-sync'], installPreReleaseVersion: !!_environmentService.args['pre-release'] }; + this.whenExtensionsReady = extensionManagementCLIService.installExtensions([], _environmentService.args['install-builtin-extension'], installOptions, !!_environmentService.args['force']) .then(null, error => { - logService.error(error); + _logService.error(error); }); } else { this.whenExtensionsReady = Promise.resolve(); } - const extensionsToInstall = environmentService.args['install-extension']; + const extensionsToInstall = _environmentService.args['install-extension']; if (extensionsToInstall) { const idsOrVSIX = extensionsToInstall.map(input => /\.vsix$/i.test(input) ? URI.file(isAbsolute(input) ? input : join(cwd(), input)) : input); this.whenExtensionsReady - .then(() => extensionManagementCLIService.installExtensions(idsOrVSIX, [], { isMachineScoped: !!environmentService.args['do-not-sync'], installPreReleaseVersion: !!environmentService.args['pre-release'] }, !!environmentService.args['force'])) + .then(() => extensionManagementCLIService.installExtensions(idsOrVSIX, [], { isMachineScoped: !!_environmentService.args['do-not-sync'], installPreReleaseVersion: !!_environmentService.args['pre-release'] }, !!_environmentService.args['force'])) .then(null, error => { - logService.error(error); + _logService.error(error); }); } } @@ -100,7 +78,7 @@ export class RemoteAgentEnvironmentChannel implements IServerChannel { case 'getExtensionHostExitInfo': { const args = <IGetExtensionHostExitInfoArguments>arg; - return this.extensionHostStatusService.getExitInfo(args.reconnectionToken); + return this._extensionHostStatusService.getExitInfo(args.reconnectionToken); } case 'whenExtensionsReady': { @@ -112,7 +90,7 @@ export class RemoteAgentEnvironmentChannel implements IServerChannel { await this.whenExtensionsReady; const args = <IScanExtensionsArguments>arg; const language = args.language; - this.logService.trace(`Scanning extensions using UI language: ${language}`); + this._logService.trace(`Scanning extensions using UI language: ${language}`); const uriTransformer = createURITransformer(args.remoteAuthority); const extensionDevelopmentLocations = args.extensionDevelopmentPath && args.extensionDevelopmentPath.map(url => URI.revive(uriTransformer.transformIncoming(url))); @@ -121,7 +99,7 @@ export class RemoteAgentEnvironmentChannel implements IServerChannel { let extensions = await this._scanExtensions(language, extensionDevelopmentPath); extensions = transformOutgoingURIs(extensions, uriTransformer); - this.logService.trace('Scanned Extensions', extensions); + this._logService.trace('Scanned Extensions', extensions); RemoteAgentEnvironmentChannel._massageWhenConditions(extensions); return extensions; @@ -140,8 +118,7 @@ export class RemoteAgentEnvironmentChannel implements IServerChannel { return null; } - const translations = await this._getTranslations(language); - let extension = await this._scanSingleExtension(extensionPath, isBuiltin, language, translations); + let extension = await this._scanSingleExtension(extensionPath, isBuiltin, language); if (!extension) { return null; @@ -308,44 +285,29 @@ export class RemoteAgentEnvironmentChannel implements IServerChannel { return { pid: process.pid, connectionToken: (this._connectionToken.type !== ServerConnectionTokenType.None ? this._connectionToken.value : ''), - appRoot: URI.file(this.environmentService.appRoot), - settingsPath: this.environmentService.machineSettingsResource, - logsPath: URI.file(this.environmentService.logsPath), - extensionsPath: URI.file(this.environmentService.extensionsPath!), - extensionHostLogsPath: URI.file(join(this.environmentService.logsPath, `exthost${RemoteAgentEnvironmentChannel._namePool++}`)), - globalStorageHome: this.environmentService.globalStorageHome, - workspaceStorageHome: this.environmentService.workspaceStorageHome, - localHistoryHome: this.environmentService.localHistoryHome, - userHome: this.environmentService.userHome, + appRoot: URI.file(this._environmentService.appRoot), + settingsPath: this._environmentService.machineSettingsResource, + logsPath: URI.file(this._environmentService.logsPath), + extensionsPath: URI.file(this._environmentService.extensionsPath!), + extensionHostLogsPath: URI.file(join(this._environmentService.logsPath, `exthost${RemoteAgentEnvironmentChannel._namePool++}`)), + globalStorageHome: this._environmentService.globalStorageHome, + workspaceStorageHome: this._environmentService.workspaceStorageHome, + localHistoryHome: this._environmentService.localHistoryHome, + userHome: this._environmentService.userHome, os: platform.OS, arch: process.arch, marks: performance.getMarks(), - useHostProxy: !!this.environmentService.args['use-host-proxy'] + useHostProxy: !!this._environmentService.args['use-host-proxy'] }; } - private async _getTranslations(language: string): Promise<Translations> { - const config = await getNLSConfiguration(language, this.environmentService.userDataPath); - if (InternalNLSConfiguration.is(config)) { - try { - const content = await pfs.Promises.readFile(config._translationsConfigFile, 'utf8'); - return JSON.parse(content); - } catch (err) { - return Object.create(null); - } - } else { - return Object.create(null); - } - } - private async _scanExtensions(language: string, extensionDevelopmentPath?: string[]): Promise<IExtensionDescription[]> { // Ensure that the language packs are available - const translations = await this._getTranslations(language); const [builtinExtensions, installedExtensions, developedExtensions] = await Promise.all([ - this._scanBuiltinExtensions(language, translations), - this._scanInstalledExtensions(language, translations), - this._scanDevelopedExtensions(language, translations, extensionDevelopmentPath) + this._scanBuiltinExtensions(language), + this._scanInstalledExtensions(language), + this._scanDevelopedExtensions(language, extensionDevelopmentPath) ]); let result = new Map<string, IExtensionDescription>(); @@ -379,107 +341,30 @@ export class RemoteAgentEnvironmentChannel implements IServerChannel { return r; } - private async _scanDevelopedExtensions(language: string, translations: Translations, extensionDevelopmentPaths?: string[]): Promise<IExtensionDescription[]> { + private async _scanDevelopedExtensions(language: string, extensionDevelopmentPaths?: string[]): Promise<IExtensionDescription[]> { if (extensionDevelopmentPaths) { - const targetPlatform = await this._extensionManagementService.getTargetPlatform(); - const extDescsP = extensionDevelopmentPaths.map(extDevPath => { - return ExtensionScanner.scanOneOrMultipleExtensions( - new ExtensionScannerInput( - this.productService.version, - this.productService.date, - this.productService.commit, - language, - true, // dev mode - extDevPath, - false, // isBuiltin - true, // isUnderDevelopment - targetPlatform, - translations // translations - ), - this.logService, - this._fileService - ); - }); - - const extDescArrays = await Promise.all(extDescsP); - let extDesc: IExtensionDescription[] = []; - for (let eds of extDescArrays) { - extDesc = extDesc.concat(eds); - } - return extDesc; + return (await Promise.all(extensionDevelopmentPaths.map(extensionDevelopmentPath => this._extensionsScannerService.scanOneOrMultipleExtensions(URI.file(resolve(extensionDevelopmentPath)), ExtensionType.User, { language })))) + .flat() + .map(e => toExtensionDescription(e, true)); } return []; } - private async _scanBuiltinExtensions(language: string, translations: Translations): Promise<IExtensionDescription[]> { - const version = this.productService.version; - const commit = this.productService.commit; - const date = this.productService.date; - const devMode = !!process.env['VSCODE_DEV']; - const targetPlatform = await this._extensionManagementService.getTargetPlatform(); - - const input = new ExtensionScannerInput(version, date, commit, language, devMode, getSystemExtensionsRoot(), true, false, targetPlatform, translations); - const builtinExtensions = ExtensionScanner.scanExtensions(input, this.logService, this._fileService); - let finalBuiltinExtensions: Promise<IExtensionDescription[]> = builtinExtensions; - - if (devMode) { - - class ExtraBuiltInExtensionResolver implements IExtensionResolver { - constructor(private builtInExtensions: IBuiltInExtension[]) { } - resolveExtensions(): Promise<IExtensionReference[]> { - return Promise.resolve(this.builtInExtensions.map((ext) => { - return { name: ext.name, path: join(getExtraDevSystemExtensionsRoot(), ext.name) }; - })); - } - } - - const builtInExtensions = Promise.resolve(this.productService.builtInExtensions || []); - - const input = new ExtensionScannerInput(version, date, commit, language, devMode, getExtraDevSystemExtensionsRoot(), true, false, targetPlatform, {}); - const extraBuiltinExtensions = builtInExtensions - .then((builtInExtensions) => new ExtraBuiltInExtensionResolver(builtInExtensions)) - .then(resolver => ExtensionScanner.scanExtensions(input, this.logService, this._fileService, resolver)); - - finalBuiltinExtensions = ExtensionScanner.mergeBuiltinExtensions(builtinExtensions, extraBuiltinExtensions); - } - - return finalBuiltinExtensions; + private async _scanBuiltinExtensions(language: string): Promise<IExtensionDescription[]> { + const scannedExtensions = await this._extensionsScannerService.scanSystemExtensions({ language, useCache: true }); + return scannedExtensions.map(e => toExtensionDescription(e, false)); } - private async _scanInstalledExtensions(language: string, translations: Translations): Promise<IExtensionDescription[]> { - const targetPlatform = await this._extensionManagementService.getTargetPlatform(); - const devMode = !!process.env['VSCODE_DEV']; - const input = new ExtensionScannerInput( - this.productService.version, - this.productService.date, - this.productService.commit, - language, - devMode, - this.environmentService.extensionsPath!, - false, // isBuiltin - false, // isUnderDevelopment - targetPlatform, - translations - ); - - return ExtensionScanner.scanExtensions(input, this.logService, this._fileService); + private async _scanInstalledExtensions(language: string): Promise<IExtensionDescription[]> { + const scannedExtensions = await this._extensionsScannerService.scanUserExtensions({ language, useCache: true }); + return scannedExtensions.map(e => toExtensionDescription(e, false)); } - private async _scanSingleExtension(extensionPath: string, isBuiltin: boolean, language: string, translations: Translations): Promise<IExtensionDescription | null> { - const targetPlatform = await this._extensionManagementService.getTargetPlatform(); - const devMode = !!process.env['VSCODE_DEV']; - const input = new ExtensionScannerInput( - this.productService.version, - this.productService.date, - this.productService.commit, - language, - devMode, - extensionPath, - isBuiltin, - false, // isUnderDevelopment - targetPlatform, - translations - ); - return ExtensionScanner.scanSingleExtension(input, this.logService, this._fileService); + private async _scanSingleExtension(extensionPath: string, isBuiltin: boolean, language: string): Promise<IExtensionDescription | null> { + const extensionLocation = URI.file(resolve(extensionPath)); + const type = isBuiltin ? ExtensionType.System : ExtensionType.User; + const scannedExtension = await this._extensionsScannerService.scanExistingExtension(extensionLocation, type, { language }); + return scannedExtension ? toExtensionDescription(scannedExtension, false) : null; } + } diff --git a/src/vs/server/node/remoteExtensionHostAgentCli.ts b/src/vs/server/node/remoteExtensionHostAgentCli.ts index c86ca327bdd..f1e8986f5c5 100644 --- a/src/vs/server/node/remoteExtensionHostAgentCli.ts +++ b/src/vs/server/node/remoteExtensionHostAgentCli.ts @@ -40,6 +40,8 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity' import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; import { buildHelpMessage, buildVersionMessage, OptionDescriptions } from 'vs/platform/environment/node/argv'; import { isWindows } from 'vs/base/common/platform'; +import { IExtensionsScannerService } from 'vs/platform/extensionManagement/common/extensionsScannerService'; +import { ExtensionsScannerService } from 'vs/server/node/extensionsScannerService'; class CliMain extends Disposable { @@ -98,6 +100,7 @@ class CliMain extends Disposable { services.set(IDownloadService, new SyncDescriptor(DownloadService)); services.set(ITelemetryService, NullTelemetryService); services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryServiceWithNoStorageService)); + services.set(IExtensionsScannerService, new SyncDescriptor(ExtensionsScannerService)); services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); services.set(IExtensionManagementCLIService, new SyncDescriptor(ExtensionManagementCLIService)); services.set(ILocalizationsService, new SyncDescriptor(LocalizationsService)); diff --git a/src/vs/server/node/remoteExtensionHostAgentServer.ts b/src/vs/server/node/remoteExtensionHostAgentServer.ts index b18fa04dd86..224d368bc38 100644 --- a/src/vs/server/node/remoteExtensionHostAgentServer.ts +++ b/src/vs/server/node/remoteExtensionHostAgentServer.ts @@ -11,7 +11,7 @@ import { performance } from 'perf_hooks'; import * as url from 'url'; import { LoaderStats } from 'vs/base/common/amd'; import { VSBuffer } from 'vs/base/common/buffer'; -import { setUnexpectedErrorHandler } from 'vs/base/common/errors'; +import { onUnexpectedError, setUnexpectedErrorHandler } from 'vs/base/common/errors'; import { isEqualOrParent } from 'vs/base/common/extpath'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { connectionTokenQueryName, FileAccess, Schemas } from 'vs/base/common/network'; @@ -35,7 +35,7 @@ import { ManagementConnection } from 'vs/server/node/remoteExtensionManagement'; import { determineServerConnectionToken, requestHasValidConnectionToken as httpRequestHasValidConnectionToken, ServerConnectionToken, ServerConnectionTokenParseError, ServerConnectionTokenType } from 'vs/server/node/serverConnectionToken'; import { IServerEnvironmentService, ServerParsedArgs } from 'vs/server/node/serverEnvironmentService'; import { setupServerServices, SocketServer } from 'vs/server/node/serverServices'; -import { serveError, serveFile, WebClientServer } from 'vs/server/node/webClientServer'; +import { CacheControl, serveError, serveFile, WebClientServer } from 'vs/server/node/webClientServer'; const SHUTDOWN_TIMEOUT = 5 * 60 * 1000; @@ -151,8 +151,7 @@ export class RemoteExtensionHostAgentServer extends Disposable implements IServe if (requestOrigin && this._webEndpointOriginChecker.matches(requestOrigin)) { responseHeaders['Access-Control-Allow-Origin'] = requestOrigin; } - - return serveFile(this._logService, req, res, filePath, responseHeaders); + return serveFile(filePath, CacheControl.ETAG, this._logService, req, res, responseHeaders); } // workbench web UI @@ -672,6 +671,13 @@ export async function createServer(address: string | net.AddressInfo | null, arg } logService.error(err); }); + process.on('SIGPIPE', () => { + // See https://github.com/microsoft/vscode-remote-release/issues/6543 + // We would normally install a SIGPIPE listener in bootstrap.js + // But in certain situations, the console itself can be in a broken pipe state + // so logging SIGPIPE to the console will cause an infinite async loop + onUnexpectedError(new Error(`Unexpected SIGPIPE`)); + }); }); // diff --git a/src/vs/server/node/remoteTerminalChannel.ts b/src/vs/server/node/remoteTerminalChannel.ts index 792b3a0274f..5c14bf3006c 100644 --- a/src/vs/server/node/remoteTerminalChannel.ts +++ b/src/vs/server/node/remoteTerminalChannel.ts @@ -29,13 +29,15 @@ import { AbstractVariableResolverService } from 'vs/workbench/services/configura import { buildUserEnvironment } from 'vs/server/node/extensionHostConnection'; import { IServerEnvironmentService } from 'vs/server/node/serverEnvironmentService'; import { IProductService } from 'vs/platform/product/common/productService'; +import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; class CustomVariableResolver extends AbstractVariableResolverService { constructor( env: platform.IProcessEnvironment, workspaceFolders: IWorkspaceFolder[], activeFileResource: URI | undefined, - resolvedVariables: { [name: string]: string } + resolvedVariables: { [name: string]: string }, + extensionService: IExtensionManagementService, ) { super({ getFolderUri: (folderName: string): URI | undefined => { @@ -68,7 +70,12 @@ class CustomVariableResolver extends AbstractVariableResolverService { }, getLineNumber: (): string | undefined => { return resolvedVariables['lineNumber']; - } + }, + getExtension: async id => { + const installed = await extensionService.getInstalled(); + const found = installed.find(e => e.identifier.id === id); + return found && { extensionLocation: found.location }; + }, }, undefined, Promise.resolve(os.homedir()), Promise.resolve(env)); } } @@ -89,7 +96,8 @@ export class RemoteTerminalChannel extends Disposable implements IServerChannel< private readonly _environmentService: IServerEnvironmentService, private readonly _logService: ILogService, private readonly _ptyService: IPtyService, - private readonly _productService: IProductService + private readonly _productService: IProductService, + private readonly _extensionManagementService: IExtensionManagementService, ) { super(); } @@ -196,16 +204,16 @@ export class RemoteTerminalChannel extends Disposable implements IServerChannel< const workspaceFolders = args.workspaceFolders.map(reviveWorkspaceFolder); const activeWorkspaceFolder = args.activeWorkspaceFolder ? reviveWorkspaceFolder(args.activeWorkspaceFolder) : undefined; const activeFileResource = args.activeFileResource ? URI.revive(uriTransformer.transformIncoming(args.activeFileResource)) : undefined; - const customVariableResolver = new CustomVariableResolver(baseEnv, workspaceFolders, activeFileResource, args.resolvedVariables); + const customVariableResolver = new CustomVariableResolver(baseEnv, workspaceFolders, activeFileResource, args.resolvedVariables, this._extensionManagementService); const variableResolver = terminalEnvironment.createVariableResolver(activeWorkspaceFolder, process.env, customVariableResolver); // Get the initial cwd - const initialCwd = terminalEnvironment.getCwd(shellLaunchConfig, os.homedir(), variableResolver, activeWorkspaceFolder?.uri, args.configuration['terminal.integrated.cwd'], this._logService); + const initialCwd = await terminalEnvironment.getCwd(shellLaunchConfig, os.homedir(), variableResolver, activeWorkspaceFolder?.uri, args.configuration['terminal.integrated.cwd'], this._logService); shellLaunchConfig.cwd = initialCwd; const envPlatformKey = platform.isWindows ? 'terminal.integrated.env.windows' : (platform.isMacintosh ? 'terminal.integrated.env.osx' : 'terminal.integrated.env.linux'); const envFromConfig = args.configuration[envPlatformKey]; - const env = terminalEnvironment.createTerminalEnvironment( + const env = await terminalEnvironment.createTerminalEnvironment( shellLaunchConfig, envFromConfig, variableResolver, @@ -222,7 +230,7 @@ export class RemoteTerminalChannel extends Disposable implements IServerChannel< } const envVariableCollections = new Map<string, IEnvironmentVariableCollection>(entries); const mergedCollection = new MergedEnvironmentVariableCollection(envVariableCollections); - mergedCollection.applyToProcessEnvironment(env); + await mergedCollection.applyToProcessEnvironment(env, variableResolver); } // Fork the process and listen for messages diff --git a/src/vs/server/node/server.cli.ts b/src/vs/server/node/server.cli.ts index 31365a6d00a..e9befdefbf5 100644 --- a/src/vs/server/node/server.cli.ts +++ b/src/vs/server/node/server.cli.ts @@ -41,7 +41,7 @@ const isSupportedForCmd = (optionId: keyof RemoteParsedArgs) => { case 'extensions-dir': case 'export-default-configuration': case 'install-source': - case 'driver': + case 'enable-smoke-test-driver': case 'extensions-download-dir': case 'builtin-extensions-dir': case 'telemetry': diff --git a/src/vs/server/node/serverEnvironmentService.ts b/src/vs/server/node/serverEnvironmentService.ts index c6d001bb680..f291461470d 100644 --- a/src/vs/server/node/serverEnvironmentService.ts +++ b/src/vs/server/node/serverEnvironmentService.ts @@ -26,12 +26,12 @@ export const serverOptions: OptionDescriptions<ServerParsedArgs> = { 'print-ip-address': { type: 'boolean' }, 'accept-server-license-terms': { type: 'boolean', cat: 'o', description: nls.localize('acceptLicenseTerms', "If set, the user accepts the server license terms and the server will be started without a user prompt.") }, 'server-data-dir': { type: 'string', cat: 'o', description: nls.localize('serverDataDir', "Specifies the directory that server data is kept in.") }, - 'telemetry-level': { type: 'string', cat: 'o', args: 'level', description: nls.localize('telemetry-level', "Sets the initial telemetry level. Valid levels are: 'off', 'crash', 'error' and 'all'. If not specified, the server will await a connection before sending any telemetry. Setting this to 'off' is equivalent to --disable-telemetry") }, + 'telemetry-level': { type: 'string', cat: 'o', args: 'level', description: nls.localize('telemetry-level', "Sets the initial telemetry level. Valid levels are: 'off', 'crash', 'error' and 'all'. If not specified, the server will send telemetry until a client connects, it will then use the clients telemetry setting. Setting this to 'off' is equivalent to --disable-telemetry") }, /* ----- vs code options --- -- */ 'user-data-dir': OPTIONS['user-data-dir'], - 'driver': OPTIONS['driver'], + 'enable-smoke-test-driver': OPTIONS['enable-smoke-test-driver'], 'disable-telemetry': OPTIONS['disable-telemetry'], 'disable-workspace-trust': OPTIONS['disable-workspace-trust'], 'file-watcher-polling': { type: 'string', deprecates: ['fileWatcherPolling'] }, @@ -140,7 +140,7 @@ export interface ServerParsedArgs { 'user-data-dir'?: string; - driver?: string; + 'enable-smoke-test-driver'?: boolean; 'disable-telemetry'?: boolean; 'file-watcher-polling'?: string; diff --git a/src/vs/server/node/serverServices.ts b/src/vs/server/node/serverServices.ts index e69a75f24ca..a072fa8bb0e 100644 --- a/src/vs/server/node/serverServices.ts +++ b/src/vs/server/node/serverServices.ts @@ -68,6 +68,8 @@ import { REMOTE_TERMINAL_CHANNEL_NAME } from 'vs/workbench/contrib/terminal/comm import { RemoteExtensionLogFileName } from 'vs/workbench/services/remote/common/remoteAgentService'; import { REMOTE_FILE_SYSTEM_CHANNEL_NAME } from 'vs/workbench/services/remote/common/remoteFileSystemProviderClient'; import { ExtensionHostStatusService, IExtensionHostStatusService } from 'vs/server/node/extensionHostStatusService'; +import { IExtensionsScannerService } from 'vs/platform/extensionManagement/common/extensionsScannerService'; +import { ExtensionsScannerService } from 'vs/server/node/extensionsScannerService'; const eventPrefix = 'monacoworkbench'; @@ -131,7 +133,7 @@ export async function setupServerServices(connectionToken: ServerConnectionToken piiPaths: getPiiPathsFromEnvironment(environmentService) }; const initialTelemetryLevelArg = environmentService.args['telemetry-level']; - let injectedTelemetryLevel: TelemetryLevel | undefined = undefined; + let injectedTelemetryLevel: TelemetryLevel = TelemetryLevel.USAGE; // Convert the passed in CLI argument into a telemetry level for the telemetry service if (initialTelemetryLevelArg === 'all') { injectedTelemetryLevel = TelemetryLevel.USAGE; @@ -152,6 +154,7 @@ export async function setupServerServices(connectionToken: ServerConnectionToken const downloadChannel = socketServer.getChannel('download', router); services.set(IDownloadService, new DownloadServiceChannelClient(downloadChannel, () => getUriTransformer('renderer') /* TODO: @Sandy @Joao need dynamic context based router */)); + services.set(IExtensionsScannerService, new SyncDescriptor(ExtensionsScannerService)); services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); const instantiationService: IInstantiationService = new InstantiationService(services); @@ -176,13 +179,14 @@ export async function setupServerServices(connectionToken: ServerConnectionToken instantiationService.invokeFunction(accessor => { const extensionManagementService = accessor.get(IExtensionManagementService); - const remoteExtensionEnvironmentChannel = new RemoteAgentEnvironmentChannel(connectionToken, environmentService, extensionManagementCLIService, extensionManagementService, logService, productService, extensionHostStatusService, fileService); + const extensionsScannerService = accessor.get(IExtensionsScannerService); + const remoteExtensionEnvironmentChannel = new RemoteAgentEnvironmentChannel(connectionToken, environmentService, extensionManagementCLIService, logService, extensionHostStatusService, extensionsScannerService); socketServer.registerChannel('remoteextensionsenvironment', remoteExtensionEnvironmentChannel); const telemetryChannel = new ServerTelemetryChannel(accessor.get(IServerTelemetryService), appInsightsAppender); socketServer.registerChannel('telemetry', telemetryChannel); - socketServer.registerChannel(REMOTE_TERMINAL_CHANNEL_NAME, new RemoteTerminalChannel(environmentService, logService, ptyService, productService)); + socketServer.registerChannel(REMOTE_TERMINAL_CHANNEL_NAME, new RemoteTerminalChannel(environmentService, logService, ptyService, productService, extensionManagementService)); const remoteFileSystemChannel = new RemoteAgentFileSystemProviderChannel(logService, environmentService); socketServer.registerChannel(REMOTE_FILE_SYSTEM_CHANNEL_NAME, remoteFileSystemChannel); diff --git a/src/vs/server/node/webClientServer.ts b/src/vs/server/node/webClientServer.ts index 7fea1489691..11e361ee77d 100644 --- a/src/vs/server/node/webClientServer.ts +++ b/src/vs/server/node/webClientServer.ts @@ -3,11 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; +import { promises as fsp, createReadStream } from 'fs'; import * as path from 'path'; import * as http from 'http'; import * as url from 'url'; -import * as util from 'util'; import * as cookie from 'cookie'; import * as crypto from 'crypto'; import { isEqualOrParent } from 'vs/base/common/extpath'; @@ -20,13 +19,14 @@ import { FileAccess, connectionTokenCookieName, connectionTokenQueryName, Schema import { generateUuid } from 'vs/base/common/uuid'; import { IProductService } from 'vs/platform/product/common/productService'; import { ServerConnectionToken, ServerConnectionTokenType } from 'vs/server/node/serverConnectionToken'; -import { asText, IRequestService } from 'vs/platform/request/common/request'; +import { asTextOrError, IRequestService } from 'vs/platform/request/common/request'; import { IHeaders } from 'vs/base/parts/request/common/request'; import { CancellationToken } from 'vs/base/common/cancellation'; import { URI } from 'vs/base/common/uri'; import { streamToBuffer } from 'vs/base/common/buffer'; import { IProductConfiguration } from 'vs/base/common/product'; import { isString } from 'vs/base/common/types'; +import { CharCode } from 'vs/base/common/charCode'; const textMimeType = { '.html': 'text/html', @@ -44,32 +44,44 @@ export async function serveError(req: http.IncomingMessage, res: http.ServerResp res.end(errorMessage); } +export const enum CacheControl { + NO_CACHING, ETAG, NO_EXPIRY +} + /** * Serve a file at a given path or 404 if the file is missing. */ -export async function serveFile(logService: ILogService, req: http.IncomingMessage, res: http.ServerResponse, filePath: string, responseHeaders: Record<string, string> = Object.create(null)): Promise<void> { +export async function serveFile(filePath: string, cacheControl: CacheControl, logService: ILogService, req: http.IncomingMessage, res: http.ServerResponse, responseHeaders: Record<string, string>): Promise<void> { try { - const stat = await util.promisify(fs.stat)(filePath); + const stat = await fsp.stat(filePath); // throws an error if file doesn't exist + if (cacheControl === CacheControl.ETAG) { + + // Check if file modified since + const etag = `W/"${[stat.ino, stat.size, stat.mtime.getTime()].join('-')}"`; // weak validator (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag) + if (req.headers['if-none-match'] === etag) { + res.writeHead(304); + return res.end(); + } - // Check if file modified since - const etag = `W/"${[stat.ino, stat.size, stat.mtime.getTime()].join('-')}"`; // weak validator (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag) - if (req.headers['if-none-match'] === etag) { - res.writeHead(304); - return res.end(); + responseHeaders['Etag'] = etag; + } else if (cacheControl === CacheControl.NO_EXPIRY) { + responseHeaders['Cache-Control'] = 'public, max-age=31536000'; + } else if (cacheControl === CacheControl.NO_CACHING) { + responseHeaders['Cache-Control'] = 'no-store'; } - // Headers responseHeaders['Content-Type'] = textMimeType[extname(filePath)] || getMediaMime(filePath) || 'text/plain'; - responseHeaders['Etag'] = etag; res.writeHead(200, responseHeaders); // Data - fs.createReadStream(filePath).pipe(res); + createReadStream(filePath).pipe(res); } catch (error) { if (error.code !== 'ENOENT') { logService.error(error); console.error(error.toString()); + } else { + console.error(`File not found: ${filePath}`); } res.writeHead(404, { 'Content-Type': 'text/plain' }); @@ -83,6 +95,9 @@ export class WebClientServer { private readonly _webExtensionResourceUrlTemplate: URI | undefined; + private readonly _staticRoute: string; + private readonly _callbackRoute: string; + constructor( private readonly _connectionToken: ServerConnectionToken, @IServerEnvironmentService private readonly _environmentService: IServerEnvironmentService, @@ -91,6 +106,9 @@ export class WebClientServer { @IProductService private readonly _productService: IProductService, ) { this._webExtensionResourceUrlTemplate = this._productService.extensionsGallery?.resourceUrlTemplate ? URI.parse(this._productService.extensionsGallery.resourceUrlTemplate) : undefined; + const qualityAndCommit = `${_productService.quality ?? 'oss'}-${_productService.commit ?? 'dev'}`; + this._staticRoute = `/${qualityAndCommit}/static`; + this._callbackRoute = `/${qualityAndCommit}/callback`; } /** @@ -102,16 +120,13 @@ export class WebClientServer { try { const pathname = parsedUrl.pathname!; - if (pathname === '/favicon.ico' || pathname === '/manifest.json' || pathname === '/code-192.png' || pathname === '/code-512.png') { - return serveFile(this._logService, req, res, join(APP_ROOT, 'resources', 'server', pathname.substr(1))); - } - if (/^\/static\//.test(pathname)) { + if (pathname.startsWith(this._staticRoute) && pathname.charCodeAt(this._staticRoute.length) === CharCode.Slash) { return this._handleStatic(req, res, parsedUrl); } if (pathname === '/') { return this._handleRoot(req, res, parsedUrl); } - if (pathname === '/callback') { + if (pathname === this._callbackRoute) { // callback support return this._handleCallback(res); } @@ -128,23 +143,22 @@ export class WebClientServer { return serveError(req, res, 500, 'Internal Server Error.'); } } - /** * Handle HTTP requests for /static/* */ private async _handleStatic(req: http.IncomingMessage, res: http.ServerResponse, parsedUrl: url.UrlWithParsedQuery): Promise<void> { const headers: Record<string, string> = Object.create(null); - // Strip `/static/` from the path + // Strip the this._staticRoute from the path const normalizedPathname = decodeURIComponent(parsedUrl.pathname!); // support paths that are uri-encoded (e.g. spaces => %20) - const relativeFilePath = normalize(normalizedPathname.substr('/static/'.length)); + const relativeFilePath = normalizedPathname.substring(this._staticRoute.length + 1); - const filePath = join(APP_ROOT, relativeFilePath); + const filePath = join(APP_ROOT, relativeFilePath); // join also normalizes the path if (!isEqualOrParent(filePath, APP_ROOT, !isLinux)) { return serveError(req, res, 400, `Bad request.`); } - return serveFile(this._logService, req, res, filePath, headers); + return serveFile(filePath, this._environmentService.isBuilt ? CacheControl.NO_EXPIRY : CacheControl.ETAG, this._logService, req, res, headers); } private _getResourceURLTemplateAuthority(uri: URI): string | undefined { @@ -197,7 +211,7 @@ export class WebClientServer { if (status !== 200) { let text: string | null = null; try { - text = await asText(context); + text = await asTextOrError(context); } catch (error) {/* Ignore */ } return serveError(req, res, status, text || `Request failed with status ${status}`); } @@ -222,9 +236,6 @@ export class WebClientServer { * Handle HTTP requests for / */ private async _handleRoot(req: http.IncomingMessage, res: http.ServerResponse, parsedUrl: url.UrlWithParsedQuery): Promise<void> { - if (!req.headers.host) { - return serveError(req, res, 400, `Bad request.`); - } const queryConnectionToken = parsedUrl.query[connectionTokenQueryName]; if (typeof queryConnectionToken === 'string') { @@ -253,14 +264,21 @@ export class WebClientServer { return res.end(); } - const remoteAuthority = req.headers.host; + let originalHost = req.headers['x-original-host']; + if (Array.isArray(originalHost)) { + originalHost = originalHost[0]; + } + const remoteAuthority = originalHost || req.headers.host; + if (!remoteAuthority) { + return serveError(req, res, 400, `Bad request.`); + } - function escapeAttribute(value: string): string { - return value.replace(/"/g, '"'); + function asJSON(value: unknown): string { + return JSON.stringify(value).replace(/"/g, '"'); } let _wrapWebWorkerExtHostInIframe: undefined | false = undefined; - if (this._environmentService.driverHandle) { + if (this._environmentService.args['enable-smoke-test-driver']) { // integration tests run at a time when the built output is not yet published to the CDN // so we must disable the iframe wrapping because the iframe URL will give a 404 _wrapWebWorkerExtHostInIframe = false; @@ -275,28 +293,45 @@ export class WebClientServer { accessToken: this._environmentService.args['github-auth'], scopes: [['user:email'], ['repo']] } : undefined; - const data = (await util.promisify(fs.readFile)(filePath)).toString() - .replace('{{WORKBENCH_WEB_CONFIGURATION}}', escapeAttribute(JSON.stringify({ - remoteAuthority, - _wrapWebWorkerExtHostInIframe, - developmentOptions: { enableSmokeTestDriver: this._environmentService.driverHandle === 'web' ? true : undefined }, - settingsSyncOptions: !this._environmentService.isBuilt && this._environmentService.args['enable-sync'] ? { enabled: true } : undefined, - enableWorkspaceTrust: !this._environmentService.args['disable-workspace-trust'], - folderUri: resolveWorkspaceURI(this._environmentService.args['default-folder']), - workspaceUri: resolveWorkspaceURI(this._environmentService.args['default-workspace']), - productConfiguration: <Partial<IProductConfiguration>>{ - embedderIdentifier: 'server-distro', - extensionsGallery: this._webExtensionResourceUrlTemplate ? { - ...this._productService.extensionsGallery, - 'resourceUrlTemplate': this._webExtensionResourceUrlTemplate.with({ - scheme: 'http', - authority: remoteAuthority, - path: `web-extension-resource/${this._webExtensionResourceUrlTemplate.authority}${this._webExtensionResourceUrlTemplate.path}` - }).toString(true) - } : undefined - } - }))) - .replace('{{WORKBENCH_AUTH_SESSION}}', () => authSessionInfo ? escapeAttribute(JSON.stringify(authSessionInfo)) : ''); + + + const workbenchWebConfiguration = { + remoteAuthority, + _wrapWebWorkerExtHostInIframe, + developmentOptions: { enableSmokeTestDriver: this._environmentService.args['enable-smoke-test-driver'] ? true : undefined }, + settingsSyncOptions: !this._environmentService.isBuilt && this._environmentService.args['enable-sync'] ? { enabled: true } : undefined, + enableWorkspaceTrust: !this._environmentService.args['disable-workspace-trust'], + folderUri: resolveWorkspaceURI(this._environmentService.args['default-folder']), + workspaceUri: resolveWorkspaceURI(this._environmentService.args['default-workspace']), + productConfiguration: <Partial<IProductConfiguration>>{ + embedderIdentifier: 'server-distro', + extensionsGallery: this._webExtensionResourceUrlTemplate ? { + ...this._productService.extensionsGallery, + 'resourceUrlTemplate': this._webExtensionResourceUrlTemplate.with({ + scheme: 'http', + authority: remoteAuthority, + path: `web-extension-resource/${this._webExtensionResourceUrlTemplate.authority}${this._webExtensionResourceUrlTemplate.path}` + }).toString(true) + } : undefined + }, + callbackRoute: this._callbackRoute + }; + + const values: { [key: string]: string } = { + WORKBENCH_WEB_CONFIGURATION: asJSON(workbenchWebConfiguration), + WORKBENCH_AUTH_SESSION: authSessionInfo ? asJSON(authSessionInfo) : '', + WORKBENCH_WEB_BASE_URL: this._staticRoute, + }; + + + let data; + try { + const workbenchTemplate = (await fsp.readFile(filePath)).toString(); + data = workbenchTemplate.replace(/\{\{([^}]+)\}\}/g, (_, key) => values[key] ?? 'undefined'); + } catch (e) { + res.writeHead(404, { 'Content-Type': 'text/plain' }); + return res.end('Not found'); + } const cspDirectives = [ 'default-src \'self\';', @@ -358,7 +393,7 @@ export class WebClientServer { */ private async _handleCallback(res: http.ServerResponse): Promise<void> { const filePath = FileAccess.asFileUri('vs/code/browser/workbench/callback.html', require).fsPath; - const data = (await util.promisify(fs.readFile)(filePath)).toString(); + const data = (await fsp.readFile(filePath)).toString(); const cspDirectives = [ 'default-src \'self\';', 'img-src \'self\' https: data: blob:;', diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts index 37afee52e07..2fa247a959b 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -64,6 +64,7 @@ import './mainThreadWorkspace'; import './mainThreadComments'; import './mainThreadNotebook'; import './mainThreadNotebookKernels'; +import './mainThreadNotebookProxyKernels'; import './mainThreadNotebookDocumentsAndEditors'; import './mainThreadNotebookRenderers'; import './mainThreadInteractive'; diff --git a/src/vs/workbench/api/browser/mainThreadAuthentication.ts b/src/vs/workbench/api/browser/mainThreadAuthentication.ts index 77c903cdf0c..7321fff0350 100644 --- a/src/vs/workbench/api/browser/mainThreadAuthentication.ts +++ b/src/vs/workbench/api/browser/mainThreadAuthentication.ts @@ -65,7 +65,7 @@ export class MainThreadAuthenticationProvider extends Disposable implements IAut quickPick.items = items; quickPick.selectedItems = items.filter(item => item.extension.allowed === undefined || item.extension.allowed); quickPick.title = nls.localize('manageTrustedExtensions', "Manage Trusted Extensions"); - quickPick.placeholder = nls.localize('manageExensions', "Choose which extensions can access this account"); + quickPick.placeholder = nls.localize('manageExtensions', "Choose which extensions can access this account"); quickPick.onDidAccept(() => { const updatedAllowedList = quickPick.items @@ -102,7 +102,7 @@ export class MainThreadAuthenticationProvider extends Disposable implements IAut const result = await this.dialogService.show( Severity.Info, accountUsages.length - ? nls.localize('signOutMessagve', "The account '{0}' has been used by: \n\n{1}\n\n Sign out from these extensions?", accountName, accountUsages.map(usage => usage.extensionName).join('\n')) + ? nls.localize('signOutMessage', "The account '{0}' has been used by: \n\n{1}\n\n Sign out from these extensions?", accountName, accountUsages.map(usage => usage.extensionName).join('\n')) : nls.localize('signOutMessageSimple', "Sign out of '{0}'?", accountName), [ nls.localize('signOut', "Sign Out"), diff --git a/src/vs/workbench/api/browser/mainThreadComments.ts b/src/vs/workbench/api/browser/mainThreadComments.ts index 6dbcd7fdc33..7d28c674abf 100644 --- a/src/vs/workbench/api/browser/mainThreadComments.ts +++ b/src/vs/workbench/api/browser/mainThreadComments.ts @@ -300,6 +300,7 @@ export class MainThreadCommentController { deleteCommentThread(commentThreadHandle: number) { let thread = this.getKnownThread(commentThreadHandle); this._threads.delete(commentThreadHandle); + thread.dispose(); if (thread.isDocumentCommentThread()) { this._commentService.updateComments(this._uniqueId, { @@ -314,8 +315,6 @@ export class MainThreadCommentController { changed: [] }); } - - thread.dispose(); } deleteCommentThreadMain(commentThreadId: string) { diff --git a/src/vs/workbench/api/browser/mainThreadEditorTabs.ts b/src/vs/workbench/api/browser/mainThreadEditorTabs.ts index f3677aec085..10b7782247b 100644 --- a/src/vs/workbench/api/browser/mainThreadEditorTabs.ts +++ b/src/vs/workbench/api/browser/mainThreadEditorTabs.ts @@ -21,7 +21,7 @@ import { TerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/termi import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { isEqual } from 'vs/base/common/resources'; - +import { isGroupEditorMoveEvent } from 'vs/workbench/common/editor/editorGroupModel'; interface TabInfo { tab: IEditorTabDto; @@ -386,6 +386,32 @@ export class MainThreadEditorTabs implements MainThreadEditorTabsShape { }); } + private _onDidTabMove(groupId: number, editorIndex: number, oldEditorIndex: number, editor: EditorInput) { + const tabs = this._groupLookup.get(groupId)?.tabs; + // Something wrong with the model state so we rebuild + if (!tabs) { + console.error('Invalid model for move change, rebuilding'); + this._createTabsModel(); + return; + } + + // Move tab from old index to new index + const removedTab = tabs.splice(oldEditorIndex, 1); + if (removedTab.length === 0) { + return; + } + tabs.splice(editorIndex, 0, removedTab[0]); + + // Notify exthost of move + this._proxy.$acceptTabOperation({ + kind: TabModelOperationKind.TAB_MOVE, + groupId, + tabDto: removedTab[0], + index: editorIndex, + oldIndex: oldEditorIndex + }); + } + /** * Builds the model from scratch based on the current state of the editor service. */ @@ -421,7 +447,7 @@ export class MainThreadEditorTabs implements MainThreadEditorTabsShape { } // TODOD @lramos15 Remove this after done finishing the tab model code - // private _eventToString(event: IEditorsChangeEvent): string { + // private _eventToString(event: IEditorsChangeEvent | IEditorsMoveEvent): string { // let eventString = ''; // switch (event.kind) { // case GroupModelChangeKind.GROUP_INDEX: eventString += 'GROUP_INDEX'; break; @@ -444,10 +470,12 @@ export class MainThreadEditorTabs implements MainThreadEditorTabsShape { * The main handler for the tab events * @param events The list of events to process */ - private _updateTabsModel(event: IEditorsChangeEvent): void { + private _updateTabsModel(changeEvent: IEditorsChangeEvent): void { + const event = changeEvent.event; + const groupId = changeEvent.groupId; switch (event.kind) { case GroupModelChangeKind.GROUP_ACTIVE: - if (event.groupId === this._editorGroupsService.activeGroup.id) { + if (groupId === this._editorGroupsService.activeGroup.id) { this._onDidGroupActivate(); break; } else { @@ -455,37 +483,42 @@ export class MainThreadEditorTabs implements MainThreadEditorTabsShape { } case GroupModelChangeKind.EDITOR_LABEL: if (event.editor !== undefined && event.editorIndex !== undefined) { - this._onDidTabLabelChange(event.groupId, event.editor, event.editorIndex); + this._onDidTabLabelChange(groupId, event.editor, event.editorIndex); break; } case GroupModelChangeKind.EDITOR_OPEN: if (event.editor !== undefined && event.editorIndex !== undefined) { - this._onDidTabOpen(event.groupId, event.editor, event.editorIndex); + this._onDidTabOpen(groupId, event.editor, event.editorIndex); break; } case GroupModelChangeKind.EDITOR_CLOSE: if (event.editorIndex !== undefined) { - this._onDidTabClose(event.groupId, event.editorIndex); + this._onDidTabClose(groupId, event.editorIndex); break; } case GroupModelChangeKind.EDITOR_ACTIVE: if (event.editorIndex !== undefined) { - this._onDidTabActiveChange(event.groupId, event.editorIndex); + this._onDidTabActiveChange(groupId, event.editorIndex); break; } case GroupModelChangeKind.EDITOR_DIRTY: if (event.editorIndex !== undefined && event.editor !== undefined) { - this._onDidTabDirty(event.groupId, event.editorIndex, event.editor); + this._onDidTabDirty(groupId, event.editorIndex, event.editor); break; } case GroupModelChangeKind.EDITOR_STICKY: if (event.editorIndex !== undefined && event.editor !== undefined) { - this._onDidTabPinChange(event.groupId, event.editorIndex, event.editor); + this._onDidTabPinChange(groupId, event.editorIndex, event.editor); break; } case GroupModelChangeKind.EDITOR_PIN: if (event.editorIndex !== undefined && event.editor !== undefined) { - this._onDidTabPreviewChange(event.groupId, event.editorIndex, event.editor); + this._onDidTabPreviewChange(groupId, event.editorIndex, event.editor); + break; + } + case GroupModelChangeKind.EDITOR_MOVE: + if (isGroupEditorMoveEvent(event) && event.editor && event.editorIndex !== undefined && event.oldEditorIndex !== undefined) { + this._onDidTabMove(groupId, event.editorIndex, event.oldEditorIndex, event.editor); break; } default: @@ -561,5 +594,20 @@ export class MainThreadEditorTabs implements MainThreadEditorTabsShape { // TODO @jrieken This isn't quite right how can we say true for some but not others? return results.every(result => result); } + + async $closeGroup(groupIds: number[], preserveFocus?: boolean): Promise<boolean> { + const groupCloseResults: boolean[] = []; + for (const groupId of groupIds) { + const group = this._editorGroupsService.getGroup(groupId); + if (group) { + groupCloseResults.push(await group.closeAllEditors()); + // Make sure group is empty but still there before removing it + if (group.count === 0 && this._editorGroupsService.getGroup(group.id)) { + this._editorGroupsService.removeGroup(group); + } + } + } + return groupCloseResults.every(result => result); + } //#endregion } diff --git a/src/vs/workbench/api/browser/mainThreadExtensionService.ts b/src/vs/workbench/api/browser/mainThreadExtensionService.ts index 4bc58f95e00..41309ab2bd9 100644 --- a/src/vs/workbench/api/browser/mainThreadExtensionService.ts +++ b/src/vs/workbench/api/browser/mainThreadExtensionService.ts @@ -26,6 +26,7 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { IRemoteConnectionData } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { URI, UriComponents } from 'vs/base/common/uri'; import { FileAccess } from 'vs/base/common/network'; +import { IExtensionDescriptionDelta } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; @extHostNamedCustomer(MainContext.MainThreadExtensionService) export class MainThreadExtensionService implements MainThreadExtensionServiceShape { @@ -57,6 +58,9 @@ export class MainThreadExtensionService implements MainThreadExtensionServiceSha public dispose(): void { } + $getExtension(extensionId: string) { + return this._extensionService.getExtension(extensionId); + } $activateExtension(extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise<void> { return this._internalExtensionService._activateById(extensionId, reason); } @@ -201,8 +205,8 @@ class ExtensionHostProxy implements IExtensionHostProxy { const uriComponents = await this._actual.$getCanonicalURI(remoteAuthority, uri); return (uriComponents ? URI.revive(uriComponents) : uriComponents); } - startExtensionHost(enabledExtensionIds: ExtensionIdentifier[]): Promise<void> { - return this._actual.$startExtensionHost(enabledExtensionIds); + startExtensionHost(extensionsDelta: IExtensionDescriptionDelta): Promise<void> { + return this._actual.$startExtensionHost(extensionsDelta); } extensionTestsExecute(): Promise<number> { return this._actual.$extensionTestsExecute(); @@ -222,8 +226,8 @@ class ExtensionHostProxy implements IExtensionHostProxy { updateRemoteConnectionData(connectionData: IRemoteConnectionData): Promise<void> { return this._actual.$updateRemoteConnectionData(connectionData); } - deltaExtensions(toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[]): Promise<void> { - return this._actual.$deltaExtensions(toAdd, toRemove); + deltaExtensions(extensionsDelta: IExtensionDescriptionDelta): Promise<void> { + return this._actual.$deltaExtensions(extensionsDelta); } test_latency(n: number): Promise<number> { return this._actual.$test_latency(n); diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index be97123b3c9..3b5e9139ca3 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -3,39 +3,32 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle'; -import { distinct } from 'vs/base/common/arrays'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { CancellationError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; -import { ITextModel } from 'vs/editor/common/model'; +import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { revive } from 'vs/base/common/marshalling'; +import { mixin } from 'vs/base/common/objects'; +import { URI } from 'vs/base/common/uri'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; +import { Position as EditorPosition } from 'vs/editor/common/core/position'; +import { IRange, Range as EditorRange } from 'vs/editor/common/core/range'; +import { Selection } from 'vs/editor/common/core/selection'; import * as languages from 'vs/editor/common/languages'; -import * as search from 'vs/workbench/contrib/search/common/search'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { IPosition, Position as EditorPosition } from 'vs/editor/common/core/position'; -import { Range as EditorRange, IRange } from 'vs/editor/common/core/range'; -import { ExtHostContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, MainContext, ILanguageConfigurationDto, IRegExpDto, IIndentationRuleDto, IOnEnterRuleDto, ILocationDto, IWorkspaceSymbolDto, reviveWorkspaceEditDto, IDocumentFilterDto, ILocationLinkDto, ISignatureHelpProviderMetadataDto, ILinkDto, ICallHierarchyItemDto, ISuggestDataDto, ICodeActionDto, ISuggestDataDtoField, ISuggestResultDtoField, ICodeActionProviderMetadataDto, ILanguageWordDefinitionDto, IdentifiableInlineCompletions, IdentifiableInlineCompletion, ITypeHierarchyItemDto, IInlayHintDto } from '../common/extHost.protocol'; -import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; -import { LanguageConfiguration, IndentationRule, OnEnterRule } from 'vs/editor/common/languages/languageConfiguration'; import { ILanguageService } from 'vs/editor/common/languages/language'; -import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; -import { URI } from 'vs/base/common/uri'; -import { Selection } from 'vs/editor/common/core/selection'; +import { IndentationRule, LanguageConfiguration, OnEnterRule } from 'vs/editor/common/languages/languageConfiguration'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { ITextModel } from 'vs/editor/common/model'; +import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import { decodeSemanticTokensDto } from 'vs/editor/common/services/semanticTokensDto'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { DataTransferConverter } from 'vs/workbench/api/common/shared/dataTransfer'; import * as callh from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; +import * as search from 'vs/workbench/contrib/search/common/search'; import * as typeh from 'vs/workbench/contrib/typeHierarchy/common/typeHierarchy'; -import { mixin } from 'vs/base/common/objects'; -import { decodeSemanticTokensDto } from 'vs/editor/common/services/semanticTokensDto'; -import { revive } from 'vs/base/common/marshalling'; -import { CancellationError } from 'vs/base/common/errors'; -import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; -import { Mimes } from 'vs/base/common/mime'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { performSnippetEdit } from 'vs/editor/contrib/snippet/browser/snippetController2'; -import { DataTransferConverter } from 'vs/workbench/api/common/shared/dataTransfer'; -import { extractEditorsDropData } from 'vs/workbench/browser/dnd'; -import { IDataTransfer, IDataTransferItem } from 'vs/workbench/common/dnd'; -import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; +import { ExtHostContext, ExtHostLanguageFeaturesShape, ICallHierarchyItemDto, ICodeActionDto, ICodeActionProviderMetadataDto, IdentifiableInlineCompletion, IdentifiableInlineCompletions, IDocumentFilterDto, IIndentationRuleDto, IInlayHintDto, ILanguageConfigurationDto, ILanguageWordDefinitionDto, ILinkDto, ILocationDto, ILocationLinkDto, IOnEnterRuleDto, IRegExpDto, ISignatureHelpProviderMetadataDto, ISuggestDataDto, ISuggestDataDtoField, ISuggestResultDtoField, ITypeHierarchyItemDto, IWorkspaceSymbolDto, MainContext, MainThreadLanguageFeaturesShape, reviveWorkspaceEditDto } from '../common/extHost.protocol'; @extHostNamedCustomer(MainContext.MainThreadLanguageFeatures) export class MainThreadLanguageFeatures extends Disposable implements MainThreadLanguageFeaturesShape { @@ -50,8 +43,6 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread @ILanguageService private readonly _languageService: ILanguageService, @ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService, @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, - @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, - @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { super(); @@ -84,24 +75,6 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread }); updateAllWordDefinitions(); } - - if (this._codeEditorService) { - const registerDropListenerOnEditor = (editor: ICodeEditor) => { - this._dropIntoEditorListeners.get(editor)?.dispose(); - this._dropIntoEditorListeners.set(editor, editor.onDropIntoEditor(e => this.onDropIntoEditor(editor, e.position, e.event))); - }; - - this._register(this._codeEditorService.onCodeEditorAdd(registerDropListenerOnEditor)); - - this._register(this._codeEditorService.onCodeEditorRemove(editor => { - this._dropIntoEditorListeners.get(editor)?.dispose(); - this._dropIntoEditorListeners.delete(editor); - })); - - for (const editor of this._codeEditorService.listCodeEditors()) { - registerDropListenerOnEditor(editor); - } - } } override dispose(): void { @@ -514,7 +487,7 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread }; } - $registerSuggestSupport(handle: number, selector: IDocumentFilterDto[], triggerCharacters: string[], supportsResolveDetails: boolean, displayName: string): void { + $registerCompletionsProvider(handle: number, selector: IDocumentFilterDto[], triggerCharacters: string[], supportsResolveDetails: boolean, displayName: string): void { const provider: languages.CompletionItemProvider = { triggerCharacters, _debugDisplayName: displayName, @@ -899,58 +872,6 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread } })); } - - private async onDropIntoEditor(editor: ICodeEditor, position: IPosition, dragEvent: DragEvent) { - if (!dragEvent.dataTransfer || !editor.hasModel()) { - return; - } - - const model = editor.getModel(); - const modelVersionNow = model.getVersionId(); - - const textEditorDataTransfer: IDataTransfer = new Map<string, IDataTransferItem>(); - for (const item of dragEvent.dataTransfer.items) { - if (item.kind === 'string') { - const type = item.type; - const asStringValue = new Promise<string>(resolve => item.getAsString(resolve)); - textEditorDataTransfer.set(type, { - asString: () => asStringValue, - value: undefined - }); - } - } - - if (!textEditorDataTransfer.has(Mimes.uriList.toLowerCase())) { - const editorData = (await this._instantiationService.invokeFunction(extractEditorsDropData, dragEvent)) - .filter(input => input.resource) - .map(input => input.resource!.toString()); - - if (editorData.length) { - const str = distinct(editorData).join('\n'); - textEditorDataTransfer.set(Mimes.uriList.toLowerCase(), { - asString: () => Promise.resolve(str), - value: undefined - }); - } - } - - if (textEditorDataTransfer.size === 0) { - return; - } - - const ordered = this._languageFeaturesService.documentOnDropEditProvider.ordered(model); - for (const provider of ordered) { - const edit = await provider.provideDocumentOnDropEdits(model, position, textEditorDataTransfer, CancellationToken.None); - if (editor.getModel().getVersionId() !== modelVersionNow) { - return; - } - - if (edit) { - performSnippetEdit(editor, edit); - return; - } - } - } } export class MainThreadDocumentSemanticTokensProvider implements languages.DocumentSemanticTokensProvider { diff --git a/src/vs/workbench/api/browser/mainThreadLanguages.ts b/src/vs/workbench/api/browser/mainThreadLanguages.ts index 71c8c4ab1c2..e8316e42dfb 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguages.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguages.ts @@ -68,8 +68,8 @@ export class MainThreadLanguages implements MainThreadLanguagesShape { if (!model) { return undefined; } - model.tokenizeIfCheap(position.lineNumber); - const tokens = model.getLineTokens(position.lineNumber); + model.tokenization.tokenizeIfCheap(position.lineNumber); + const tokens = model.tokenization.getLineTokens(position.lineNumber); const idx = tokens.findTokenIndexAtOffset(position.column - 1); return { type: tokens.getStandardTokenType(idx), diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index b22dce389de..6b522da7c97 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -200,14 +200,14 @@ CommandsRegistry.registerCommand('_executeDataToNotebook', async (accessor, ...a } const dto = await info.serializer.dataToNotebook(bytes); - return NotebookDto.toNotebookDataDto(dto); + return new SerializableObjectWithBuffers(NotebookDto.toNotebookDataDto(dto)); }); CommandsRegistry.registerCommand('_executeNotebookToData', async (accessor, ...args) => { const [notebookType, dto] = args; assertType(typeof notebookType === 'string', 'string'); - assertType(typeof dto === 'object', 'NotebookDataDto'); + assertType(typeof dto === 'object'); const notebookService = accessor.get(INotebookService); const info = await notebookService.withNotebookDataProvider(notebookType); @@ -215,7 +215,7 @@ CommandsRegistry.registerCommand('_executeNotebookToData', async (accessor, ...a return; } - const data = NotebookDto.fromNotebookDataDto(dto); + const data = NotebookDto.fromNotebookDataDto(dto.value); const bytes = await info.serializer.notebookToData(data); return bytes; }); diff --git a/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts b/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts index 46febd36991..a8809c58016 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts @@ -15,11 +15,12 @@ import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/ext import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/notebookEditorService'; import { INotebookCellExecution, INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; -import { INotebookKernel, INotebookKernelChangeEvent, INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { IResolvedNotebookKernel, INotebookKernelChangeEvent, INotebookKernelService, NotebookKernelType } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { SerializableObjectWithBuffers } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import { ExtHostContext, ExtHostNotebookKernelsShape, ICellExecuteUpdateDto, ICellExecutionCompleteDto, INotebookKernelDto2, MainContext, MainThreadNotebookKernelsShape } from '../common/extHost.protocol'; -abstract class MainThreadKernel implements INotebookKernel { +abstract class MainThreadKernel implements IResolvedNotebookKernel { + readonly type: NotebookKernelType.Resolved = NotebookKernelType.Resolved; private readonly _onDidChange = new Emitter<INotebookKernelChangeEvent>(); private readonly preloads: { uri: URI; provides: string[] }[]; diff --git a/src/vs/workbench/api/browser/mainThreadNotebookProxyKernels.ts b/src/vs/workbench/api/browser/mainThreadNotebookProxyKernels.ts new file mode 100644 index 00000000000..74e7290161d --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadNotebookProxyKernels.ts @@ -0,0 +1,130 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from 'vs/base/common/event'; +import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; +import { INotebookKernelService, INotebookProxyKernel, INotebookProxyKernelChangeEvent, ProxyKernelState, NotebookKernelType } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { ExtHostContext, ExtHostNotebookProxyKernelsShape, INotebookProxyKernelDto, MainContext, MainThreadNotebookProxyKernelsShape } from '../common/extHost.protocol'; +import { onUnexpectedError } from 'vs/base/common/errors'; + +abstract class MainThreadProxyKernel implements INotebookProxyKernel { + readonly type: NotebookKernelType.Proxy = NotebookKernelType.Proxy; + protected readonly _onDidChange = new Emitter<INotebookProxyKernelChangeEvent>(); + readonly onDidChange: Event<INotebookProxyKernelChangeEvent> = this._onDidChange.event; + readonly id: string; + readonly viewType: string; + readonly extension: ExtensionIdentifier; + readonly preloadProvides: string[] = []; + label: string; + description?: string; + detail?: string; + kind?: string; + supportedLanguages: string[] = []; + connectionState: ProxyKernelState; + + constructor(data: INotebookProxyKernelDto) { + this.id = data.id; + this.viewType = data.notebookType; + this.extension = data.extensionId; + + this.label = data.label; + this.description = data.description; + this.detail = data.detail; + this.kind = data.kind; + + this.connectionState = ProxyKernelState.Disconnected; + } + + update(data: Partial<INotebookProxyKernel>) { + const event: INotebookProxyKernelChangeEvent = Object.create(null); + if (data.label !== undefined) { + this.label = data.label; + event.label = true; + } + if (data.description !== undefined) { + this.description = data.description; + event.description = true; + } + if (data.detail !== undefined) { + this.detail = data.detail; + event.detail = true; + } + if (data.kind !== undefined) { + this.kind = data.kind; + event.kind = true; + } + + this._onDidChange.fire(event); + } + + abstract resolveKernel(): Promise<string | null>; +} + +@extHostNamedCustomer(MainContext.MainThreadNotebookProxyKernels) +export class MainThreadNotebookProxyKernels implements MainThreadNotebookProxyKernelsShape { + + private readonly _disposables = new DisposableStore(); + + private readonly _proxyKernels = new Map<number, [kernel: MainThreadProxyKernel, registraion: IDisposable]>(); + private readonly _proxyKernelProxy: ExtHostNotebookProxyKernelsShape; + + constructor( + extHostContext: IExtHostContext, + @INotebookKernelService private readonly _notebookKernelService: INotebookKernelService, + ) { + this._proxyKernelProxy = extHostContext.getProxy(ExtHostContext.ExtHostNotebookProxyKernels); + } + + dispose(): void { + this._disposables.dispose(); + + for (let [, registration] of this._proxyKernels.values()) { + registration.dispose(); + } + } + + // -- Proxy kernel + + async $addProxyKernel(handle: number, data: INotebookProxyKernelDto): Promise<void> { + const that = this; + const proxyKernel = new class extends MainThreadProxyKernel { + async resolveKernel(): Promise<string | null> { + try { + this.connectionState = ProxyKernelState.Initializing; + this._onDidChange.fire({ connectionState: true }); + const delegateKernel = await that._proxyKernelProxy.$resolveKernel(handle); + this.connectionState = ProxyKernelState.Connected; + this._onDidChange.fire({ connectionState: true }); + return delegateKernel; + } catch (err) { + onUnexpectedError(err); + this.connectionState = ProxyKernelState.Disconnected; + this._onDidChange.fire({ connectionState: true }); + return null; + } + } + }(data); + + const registration = this._notebookKernelService.registerKernel(proxyKernel); + this._proxyKernels.set(handle, [proxyKernel, registration]); + } + + $updateProxyKernel(handle: number, data: Partial<INotebookProxyKernelDto>): void { + const tuple = this._proxyKernels.get(handle); + if (tuple) { + tuple[0].update(data); + } + } + + $removeProxyKernel(handle: number): void { + const tuple = this._proxyKernels.get(handle); + if (tuple) { + tuple[1].dispose(); + this._proxyKernels.delete(handle); + } + } +} diff --git a/src/vs/workbench/api/browser/mainThreadOutputService.ts b/src/vs/workbench/api/browser/mainThreadOutputService.ts index 2c82719f4e8..34859bb4235 100644 --- a/src/vs/workbench/api/browser/mainThreadOutputService.ts +++ b/src/vs/workbench/api/browser/mainThreadOutputService.ts @@ -4,8 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Registry } from 'vs/platform/registry/common/platform'; -import { IOutputService, IOutputChannel, OUTPUT_VIEW_ID, OutputChannelUpdateMode } from 'vs/workbench/contrib/output/common/output'; -import { Extensions, IOutputChannelRegistry } from 'vs/workbench/services/output/common/output'; +import { Extensions, IOutputChannelRegistry, IOutputService, IOutputChannel, OUTPUT_VIEW_ID, OutputChannelUpdateMode } from 'vs/workbench/services/output/common/output'; import { MainThreadOutputServiceShape, MainContext, ExtHostOutputServiceShape, ExtHostContext } from '../common/extHost.protocol'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { UriComponents, URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/api/browser/mainThreadSecretState.ts b/src/vs/workbench/api/browser/mainThreadSecretState.ts index 1a131d41518..acd6e3db0c1 100644 --- a/src/vs/workbench/api/browser/mainThreadSecretState.ts +++ b/src/vs/workbench/api/browser/mainThreadSecretState.ts @@ -8,6 +8,7 @@ import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/ext import { ICredentialsService } from 'vs/platform/credentials/common/credentials'; import { IEncryptionService } from 'vs/workbench/services/encryption/common/encryptionService'; import { ExtHostContext, ExtHostSecretStateShape, MainContext, MainThreadSecretStateShape } from '../common/extHost.protocol'; +import { ILogService } from 'vs/platform/log/common/log'; @extHostNamedCustomer(MainContext.MainThreadSecretState) export class MainThreadSecretState extends Disposable implements MainThreadSecretStateShape { @@ -19,6 +20,7 @@ export class MainThreadSecretState extends Disposable implements MainThreadSecre extHostContext: IExtHostContext, @ICredentialsService private readonly credentialsService: ICredentialsService, @IEncryptionService private readonly encryptionService: IEncryptionService, + @ILogService private readonly logService: ILogService, ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostSecretState); @@ -36,7 +38,26 @@ export class MainThreadSecretState extends Disposable implements MainThreadSecre async $getPassword(extensionId: string, key: string): Promise<string | undefined> { const fullKey = await this.getFullKey(extensionId); const password = await this.credentialsService.getPassword(fullKey, key); - const decrypted = password && await this.encryptionService.decrypt(password); + if (!password) { + return undefined; + } + + let decrypted: string | null; + try { + decrypted = await this.encryptionService.decrypt(password); + } catch (e) { + this.logService.error(e); + + // If we are on a platform that newly started encrypting secrets before storing them, + // then passwords previously stored were stored un-encrypted (NOTE: but still being stored in a secure keyring). + // When we try to decrypt a password that wasn't encrypted previously, the encryption service will throw. + // To recover gracefully, we first try to encrypt & store the password (essentially migrating the secret to the new format) + // and then we try to read it and decrypt again. + const encryptedForSet = await this.encryptionService.encrypt(password); + await this.credentialsService.setPassword(fullKey, key, encryptedForSet); + const passwordEncrypted = await this.credentialsService.getPassword(fullKey, key); + decrypted = passwordEncrypted && await this.encryptionService.decrypt(passwordEncrypted); + } if (decrypted) { try { @@ -44,7 +65,8 @@ export class MainThreadSecretState extends Disposable implements MainThreadSecre if (value.extensionId === extensionId) { return value.content; } - } catch (_) { + } catch (e) { + this.logService.error(e); throw new Error('Cannot get password'); } } @@ -59,7 +81,7 @@ export class MainThreadSecretState extends Disposable implements MainThreadSecre content: value }); const encrypted = await this.encryptionService.encrypt(toEncrypt); - return this.credentialsService.setPassword(fullKey, key, encrypted); + return await this.credentialsService.setPassword(fullKey, key, encrypted); } async $deletePassword(extensionId: string, key: string): Promise<void> { diff --git a/src/vs/workbench/api/browser/mainThreadTask.ts b/src/vs/workbench/api/browser/mainThreadTask.ts index 60575f37b1f..9cd5c8235f9 100644 --- a/src/vs/workbench/api/browser/mainThreadTask.ts +++ b/src/vs/workbench/api/browser/mainThreadTask.ts @@ -29,7 +29,7 @@ import { ExtHostContext, MainThreadTaskShape, ExtHostTaskShape, MainContext } fr import { TaskDefinitionDTO, TaskExecutionDTO, ProcessExecutionOptionsDTO, TaskPresentationOptionsDTO, ProcessExecutionDTO, ShellExecutionDTO, ShellExecutionOptionsDTO, CustomExecutionDTO, TaskDTO, TaskSourceDTO, TaskHandleDTO, TaskFilterDTO, TaskProcessStartedDTO, TaskProcessEndedDTO, TaskSystemInfoDTO, - RunOptionsDTO + RunOptionsDTO, TaskGroupDTO } from 'vs/workbench/api/common/shared/tasks'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; @@ -320,7 +320,7 @@ namespace TaskDTO { hasDefinedMatchers: ContributedTask.is(task) ? task.hasDefinedMatchers : false, runOptions: RunOptionsDTO.from(task.runOptions), }; - result.group = TaskGroup.from(task.configurationProperties.group); + result.group = TaskGroupDTO.from(task.configurationProperties.group); if (task.configurationProperties.detail) { result.detail = task.configurationProperties.detail; @@ -389,6 +389,18 @@ namespace TaskDTO { } } +namespace TaskGroupDTO { + export function from(value: string | TaskGroup | undefined): TaskGroupDTO | undefined { + if (value === undefined) { + return undefined; + } + return { + _id: (typeof value === 'string') ? value : value._id, + isDefault: (typeof value === 'string') ? false : ((typeof value.isDefault === 'string') ? false : value.isDefault) + }; + } +} + namespace TaskFilterDTO { export function from(value: TaskFilter): TaskFilterDTO { return value; diff --git a/src/vs/workbench/api/browser/mainThreadTelemetry.ts b/src/vs/workbench/api/browser/mainThreadTelemetry.ts index 58a919fc1e6..ac89aca9280 100644 --- a/src/vs/workbench/api/browser/mainThreadTelemetry.ts +++ b/src/vs/workbench/api/browser/mainThreadTelemetry.ts @@ -3,15 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ITelemetryService, TelemetryLevel, TELEMETRY_OLD_SETTING_ID, TELEMETRY_SETTING_ID } from 'vs/platform/telemetry/common/telemetry'; -import { MainThreadTelemetryShape, MainContext, ExtHostTelemetryShape, ExtHostContext } from '../common/extHost.protocol'; -import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; -import { ClassifiedEvent, StrictPropertyCheck, GDPRClassification } from 'vs/platform/telemetry/common/gdprTypings'; import { Disposable } from 'vs/base/common/lifecycle'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IProductService } from 'vs/platform/product/common/productService'; -import { getTelemetryLevel, supportsTelemetry } from 'vs/platform/telemetry/common/telemetryUtils'; +import { ClassifiedEvent, GDPRClassification, StrictPropertyCheck } from 'vs/platform/telemetry/common/gdprTypings'; +import { ITelemetryService, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; +import { supportsTelemetry } from 'vs/platform/telemetry/common/telemetryUtils'; +import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; +import { ExtHostContext, ExtHostTelemetryShape, MainContext, MainThreadTelemetryShape } from '../common/extHost.protocol'; @extHostNamedCustomer(MainContext.MainThreadTelemetry) export class MainThreadTelemetry extends Disposable implements MainThreadTelemetryShape { @@ -22,7 +21,6 @@ export class MainThreadTelemetry extends Disposable implements MainThreadTelemet constructor( extHostContext: IExtHostContext, @ITelemetryService private readonly _telemetryService: ITelemetryService, - @IConfigurationService private readonly _configurationService: IConfigurationService, @IEnvironmentService private readonly _environmentService: IEnvironmentService, @IProductService private readonly _productService: IProductService ) { @@ -31,10 +29,8 @@ export class MainThreadTelemetry extends Disposable implements MainThreadTelemet this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTelemetry); if (supportsTelemetry(this._productService, this._environmentService)) { - this._register(this._configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(TELEMETRY_SETTING_ID) || e.affectsConfiguration(TELEMETRY_OLD_SETTING_ID)) { - this._proxy.$onDidChangeTelemetryLevel(this.telemetryLevel); - } + this._register(_telemetryService.telemetryLevel.onDidChange(level => { + this._proxy.$onDidChangeTelemetryLevel(level); })); } @@ -46,7 +42,7 @@ export class MainThreadTelemetry extends Disposable implements MainThreadTelemet return TelemetryLevel.NONE; } - return getTelemetryLevel(this._configurationService); + return this._telemetryService.telemetryLevel.value; } $publicLog(eventName: string, data: any = Object.create(null)): void { @@ -55,7 +51,7 @@ export class MainThreadTelemetry extends Disposable implements MainThreadTelemet this._telemetryService.publicLog(eventName, data); } - $publicLog2<E extends ClassifiedEvent<T> = never, T extends GDPRClassification<T> = never>(eventName: string, data: StrictPropertyCheck<T, E>): void { + $publicLog2<E extends ClassifiedEvent<T> = never, T extends GDPRClassification<T> = never>(eventName: string, data?: StrictPropertyCheck<T, E>): void { this.$publicLog(eventName, data as any); } } diff --git a/src/vs/workbench/api/browser/mainThreadTimeline.ts b/src/vs/workbench/api/browser/mainThreadTimeline.ts index 6636909bbdf..d6547f29752 100644 --- a/src/vs/workbench/api/browser/mainThreadTimeline.ts +++ b/src/vs/workbench/api/browser/mainThreadTimeline.ts @@ -9,7 +9,7 @@ import { URI } from 'vs/base/common/uri'; import { ILogService } from 'vs/platform/log/common/log'; import { MainContext, MainThreadTimelineShape, ExtHostTimelineShape, ExtHostContext } from 'vs/workbench/api/common/extHost.protocol'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; -import { TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor, ITimelineService, InternalTimelineOptions, Timeline } from 'vs/workbench/contrib/timeline/common/timeline'; +import { TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor, ITimelineService, Timeline } from 'vs/workbench/contrib/timeline/common/timeline'; import { revive } from 'vs/base/common/marshalling'; @extHostNamedCustomer(MainContext.MainThreadTimeline) @@ -40,8 +40,8 @@ export class MainThreadTimeline implements MainThreadTimelineShape { this._timelineService.registerTimelineProvider({ ...provider, onDidChange: onDidChange.event, - async provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken, internalOptions?: InternalTimelineOptions) { - return revive<Timeline>(await proxy.$getTimeline(provider.id, uri, options, token, internalOptions)); + async provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken) { + return revive<Timeline>(await proxy.$getTimeline(provider.id, uri, options, token)); }, dispose() { emitters.delete(provider.id); diff --git a/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts b/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts index 3ca501b079b..6659298ef14 100644 --- a/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts +++ b/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts @@ -174,8 +174,10 @@ export class MainThreadWebviewPanels extends Disposable implements extHostProtoc } as const; type Classification = { - extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'mjbvz'; comment: 'Id of the extension that created the webview panel' }; - viewType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'mjbvz'; comment: 'Id of the webview' }; + extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Id of the extension that created the webview panel' }; + viewType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Id of the webview' }; + owner: 'mjbvz'; + comment: 'Triggered when a webview is created. Records the type of webview and the extension which created it'; }; this._telemetryService.publicLog2<typeof payload, Classification>('webviews:createWebviewPanel', payload); @@ -275,6 +277,7 @@ export class MainThreadWebviewPanels extends Disposable implements extHostProtoc state, panelOptions: webviewInput.webview.options, webviewOptions: webviewInput.webview.contentOptions, + active: webviewInput === this._editorService.activeEditor, }, editorGroupToColumn(this._editorGroupService, webviewInput.group || 0)); } catch (error) { onUnexpectedError(error); diff --git a/src/vs/workbench/api/browser/mainThreadWebviews.ts b/src/vs/workbench/api/browser/mainThreadWebviews.ts index 15e71145210..ef088e5c054 100644 --- a/src/vs/workbench/api/browser/mainThreadWebviews.ts +++ b/src/vs/workbench/api/browser/mainThreadWebviews.ts @@ -65,8 +65,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma public async $postMessage(handle: extHostProtocol.WebviewHandle, jsonMessage: string, ...buffers: VSBuffer[]): Promise<boolean> { const webview = this.getWebview(handle); const { message, arrayBuffers } = deserializeWebviewMessage(jsonMessage, buffers); - webview.postMessage(message, arrayBuffers); - return true; + return webview.postMessage(message, arrayBuffers); } private hookupWebviewEventDelegate(handle: extHostProtocol.WebviewHandle, webview: IOverlayWebview, options: { serializeBuffersForPostMessage: boolean }) { diff --git a/src/vs/workbench/api/browser/viewsExtensionPoint.ts b/src/vs/workbench/api/browser/viewsExtensionPoint.ts index f839ed68780..489f34bb0fd 100644 --- a/src/vs/workbench/api/browser/viewsExtensionPoint.ts +++ b/src/vs/workbench/api/browser/viewsExtensionPoint.ts @@ -510,7 +510,7 @@ class ViewsExtensionHandler implements IWorkbenchContribution { containerTitle: item.contextualTitle || viewContainer?.title, canToggleVisibility: true, canMoveView: viewContainer?.id !== REMOTE, - treeView: type === ViewType.Tree ? this.instantiationService.createInstance(CustomTreeView, item.id, item.name) : undefined, + treeView: type === ViewType.Tree ? this.instantiationService.createInstance(CustomTreeView, item.id, item.name, extension.description.identifier.value) : undefined, collapsed: this.showCollapsed(container) || initialVisibility === InitialVisibility.Collapsed, order: order, extensionId: extension.description.identifier, diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index e59da75b06d..864b227de8e 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -70,7 +70,6 @@ import { IExtHostTunnelService } from 'vs/workbench/api/common/extHostTunnelServ import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; import { ExtHostAuthentication } from 'vs/workbench/api/common/extHostAuthentication'; import { ExtHostTimeline } from 'vs/workbench/api/common/extHostTimeline'; -import { ExtHostNotebookConcatDocument } from 'vs/workbench/api/common/extHostNotebookConcatDocument'; import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths'; import { IExtHostConsumerFileSystem } from 'vs/workbench/api/common/extHostFileSystemConsumer'; import { ExtHostWebviewViews } from 'vs/workbench/api/common/extHostWebviewView'; @@ -92,11 +91,17 @@ import { ExtHostNotebookEditors } from 'vs/workbench/api/common/extHostNotebookE import { ExtHostNotebookDocuments } from 'vs/workbench/api/common/extHostNotebookDocuments'; import { ExtHostInteractive } from 'vs/workbench/api/common/extHostInteractive'; import { combinedDisposable } from 'vs/base/common/lifecycle'; -import { checkProposedApiEnabled, isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; +import { checkProposedApiEnabled, ExtensionIdentifierSet, isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import { DebugConfigurationProviderTriggerKind } from 'vs/workbench/contrib/debug/common/debug'; +import { ExtHostNotebookProxyKernels } from 'vs/workbench/api/common/extHostNotebookProxyKernels'; + +export interface IExtensionRegistries { + mine: ExtensionDescriptionRegistry; + all: ExtensionDescriptionRegistry; +} export interface IExtensionApiFactory { - (extension: IExtensionDescription, registry: ExtensionDescriptionRegistry, configProvider: ExtHostConfigProvider): typeof vscode; + (extension: IExtensionDescription, extensionInfo: IExtensionRegistries, configProvider: ExtHostConfigProvider): typeof vscode; } /** @@ -156,6 +161,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostNotebookDocuments = rpcProtocol.set(ExtHostContext.ExtHostNotebookDocuments, new ExtHostNotebookDocuments(extHostNotebook)); const extHostNotebookEditors = rpcProtocol.set(ExtHostContext.ExtHostNotebookEditors, new ExtHostNotebookEditors(extHostLogService, rpcProtocol, extHostNotebook)); const extHostNotebookKernels = rpcProtocol.set(ExtHostContext.ExtHostNotebookKernels, new ExtHostNotebookKernels(rpcProtocol, initData, extHostNotebook, extHostCommands, extHostLogService)); + const extHostNotebookProxyKernels = rpcProtocol.set(ExtHostContext.ExtHostNotebookProxyKernels, new ExtHostNotebookProxyKernels(rpcProtocol, extHostNotebookKernels, extHostLogService)); const extHostNotebookRenderers = rpcProtocol.set(ExtHostContext.ExtHostNotebookRenderers, new ExtHostNotebookRenderers(rpcProtocol, extHostNotebook)); const extHostEditors = rpcProtocol.set(ExtHostContext.ExtHostEditors, new ExtHostEditors(rpcProtocol, extHostDocumentsAndEditors)); const extHostTreeViews = rpcProtocol.set(ExtHostContext.ExtHostTreeViews, new ExtHostTreeViews(rpcProtocol.getProxy(MainContext.MainThreadTreeViews), extHostCommands, extHostLogService)); @@ -195,7 +201,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I // Register API-ish commands ExtHostApiCommands.register(extHostCommands); - return function (extension: IExtensionDescription, extensionRegistry: ExtensionDescriptionRegistry, configProvider: ExtHostConfigProvider): typeof vscode { + return function (extension: IExtensionDescription, extensionInfo: IExtensionRegistries, configProvider: ExtHostConfigProvider): typeof vscode { // Check document selectors for being overly generic. Technically this isn't a problem but // in practice many extensions say they support `fooLang` but need fs-access to do so. Those @@ -359,10 +365,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I Object.freeze(env); } - const extensionKind = initData.remote.isRemote - ? extHostTypes.ExtensionKind.Workspace - : extHostTypes.ExtensionKind.UI; - + // namespace: tests const tests: typeof vscode.tests = { createTestController(provider, label, refreshHandler?: (token: vscode.CancellationToken) => Thenable<void> | void) { return extHostTesting.createTestController(provider, label, refreshHandler); @@ -386,19 +389,49 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I }; // namespace: extensions + const extensionKind = initData.remote.isRemote + ? extHostTypes.ExtensionKind.Workspace + : extHostTypes.ExtensionKind.UI; + const extensions: typeof vscode.extensions = { - getExtension(extensionId: string): vscode.Extension<any> | undefined { - const desc = extensionRegistry.getExtensionDescription(extensionId); - if (desc) { - return new Extension(extensionService, extension.identifier, desc, extensionKind); + getExtension(extensionId: string, includeFromDifferentExtensionHosts?: boolean): vscode.Extension<any> | undefined { + if (!isProposedApiEnabled(extension, 'extensionsAny')) { + includeFromDifferentExtensionHosts = false; + } + const mine = extensionInfo.mine.getExtensionDescription(extensionId); + if (mine) { + return new Extension(extensionService, extension.identifier, mine, extensionKind, false); + } + if (includeFromDifferentExtensionHosts) { + const foreign = extensionInfo.all.getExtensionDescription(extensionId); + if (foreign) { + return new Extension(extensionService, extension.identifier, foreign, extensionKind /* TODO@alexdima THIS IS WRONG */, true); + } } return undefined; }, get all(): vscode.Extension<any>[] { - return extensionRegistry.getAllExtensionDescriptions().map((desc) => new Extension(extensionService, extension.identifier, desc, extensionKind)); + const result: vscode.Extension<any>[] = []; + for (const desc of extensionInfo.mine.getAllExtensionDescriptions()) { + result.push(new Extension(extensionService, extension.identifier, desc, extensionKind, false)); + } + return result; + }, + get allAcrossExtensionHosts(): vscode.Extension<any>[] { + checkProposedApiEnabled(extension, 'extensionsAny'); + const local = new ExtensionIdentifierSet(extensionInfo.mine.getAllExtensionDescriptions().map(desc => desc.identifier)); + const result: vscode.Extension<any>[] = []; + for (const desc of extensionInfo.all.getAllExtensionDescriptions()) { + const isFromDifferentExtensionHost = !local.has(desc.identifier); + result.push(new Extension(extensionService, extension.identifier, desc, extensionKind /* TODO@alexdima THIS IS WRONG */, isFromDifferentExtensionHost)); + } + return result; }, get onDidChange() { - return extensionRegistry.onDidChange; + if (isProposedApiEnabled(extension, 'extensionsAny')) { + return Event.any(extensionInfo.mine.onDidChange, extensionInfo.all.onDidChange); + } + return extensionInfo.mine.onDidChange; } }; @@ -625,7 +658,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I showInputBox(options?: vscode.InputBoxOptions, token?: vscode.CancellationToken) { if (options?.validateInput2) { checkProposedApiEnabled(extension, 'inputBoxSeverity'); - options.validateInput = options.validateInput2 as any; } return extHostQuickOpen.showInput(options, token); }, @@ -673,7 +705,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'editorInsets'); return extHostEditorInsets.createWebviewEditorInset(editor, line, height, options, extension); }, - createTerminal(nameOrOptions?: vscode.TerminalOptions | vscode.ExtensionTerminalOptions | string, shellPath?: string, shellArgs?: string[] | string): vscode.Terminal { + createTerminal(nameOrOptions?: vscode.TerminalOptions | vscode.ExtensionTerminalOptions | string, shellPath?: string, shellArgs?: readonly string[] | string): vscode.Terminal { if (typeof nameOrOptions === 'object') { if ('pty' in nameOrOptions) { return extHostTerminalService.createExtensionTerminal(nameOrOptions); @@ -758,7 +790,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostUriOpeners.registerExternalUriOpener(extension.identifier, id, opener, metadata); }, get tabGroups(): vscode.TabGroups { - checkProposedApiEnabled(extension, 'tabs'); return extHostEditorTabs.tabGroups; }, getInlineCompletionItemController<T extends vscode.InlineCompletionItem>(provider: vscode.InlineCompletionItemProvider<T>): vscode.InlineCompletionController<T> { @@ -892,11 +923,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostNotebook.getNotebookDocument(uri).apiNotebook; }, onDidSaveNotebookDocument(listener, thisArg, disposables) { - checkProposedApiEnabled(extension, 'notebookDocumentEvents'); return extHostNotebookDocuments.onDidSaveNotebookDocument(listener, thisArg, disposables); }, onDidChangeNotebookDocument(listener, thisArg, disposables) { - checkProposedApiEnabled(extension, 'notebookDocumentEvents'); return extHostNotebookDocuments.onDidChangeNotebookDocument(listener, thisArg, disposables); }, get onDidOpenNotebookDocument(): Event<vscode.NotebookDocument> { @@ -1129,10 +1158,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'notebookCellExecutionState'); return extHostNotebookKernels.onDidChangeNotebookCellExecutionState(listener, thisArgs, disposables); }, - createConcatTextDocument(notebook, selector) { - checkProposedApiEnabled(extension, 'notebookConcatTextDocument'); - return new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook, selector); - }, + createNotebookProxyController(id: string, notebookType: string, label: string, handler: () => vscode.NotebookController | string | Thenable<vscode.NotebookController | string>) { + checkProposedApiEnabled(extension, 'notebookProxyController'); + return extHostNotebookProxyKernels.createNotebookProxyController(extension, id, notebookType, label, handler); + } }; return <typeof vscode>{ @@ -1314,13 +1343,13 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I LanguageStatusSeverity: extHostTypes.LanguageStatusSeverity, QuickPickItemKind: extHostTypes.QuickPickItemKind, InputBoxValidationSeverity: extHostTypes.InputBoxValidationSeverity, - TabKindText: extHostTypes.TextTabInput, - TabKindTextDiff: extHostTypes.TextDiffTabInput, - TabKindCustom: extHostTypes.CustomEditorTabInput, - TabKindNotebook: extHostTypes.NotebookEditorTabInput, - TabKindNotebookDiff: extHostTypes.NotebookDiffEditorTabInput, - TabKindWebview: extHostTypes.WebviewEditorTabInput, - TabKindTerminal: extHostTypes.TerminalEditorTabInput + TabInputText: extHostTypes.TextTabInput, + TabInputTextDiff: extHostTypes.TextDiffTabInput, + TabInputCustom: extHostTypes.CustomEditorTabInput, + TabInputNotebook: extHostTypes.NotebookEditorTabInput, + TabInputNotebookDiff: extHostTypes.NotebookDiffEditorTabInput, + TabInputWebview: extHostTypes.WebviewEditorTabInput, + TabInputTerminal: extHostTypes.TerminalEditorTabInput }; }; } diff --git a/src/vs/workbench/api/common/extHost.common.services.ts b/src/vs/workbench/api/common/extHost.common.services.ts index e8ec83f8d66..9ffb9e748b6 100644 --- a/src/vs/workbench/api/common/extHost.common.services.ts +++ b/src/vs/workbench/api/common/extHost.common.services.ts @@ -26,6 +26,7 @@ import { ExtHostEditorTabs, IExtHostEditorTabs } from 'vs/workbench/api/common/e import { ExtHostLoggerService } from 'vs/workbench/api/common/extHostLoggerService'; import { ILoggerService, ILogService } from 'vs/platform/log/common/log'; import { ExtHostLogService } from 'vs/workbench/api/common/extHostLogService'; +import { ExtHostVariableResolverProviderService, IExtHostVariableResolverProvider } from 'vs/workbench/api/common/extHostVariableResolverService'; registerSingleton(ILoggerService, ExtHostLoggerService); registerSingleton(ILogService, ExtHostLogService); @@ -48,3 +49,4 @@ registerSingleton(IExtHostWorkspace, ExtHostWorkspace); registerSingleton(IExtHostSecretState, ExtHostSecretState); registerSingleton(IExtHostTelemetry, ExtHostTelemetry); registerSingleton(IExtHostEditorTabs, ExtHostEditorTabs); +registerSingleton(IExtHostVariableResolverProvider, ExtHostVariableResolverProviderService); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index c33567f8d99..4787ccb401b 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -53,16 +53,16 @@ import * as notebookCommon from 'vs/workbench/contrib/notebook/common/notebookCo import { CellExecutionUpdateType } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; import { ICellExecutionComplete, ICellExecutionStateUpdate } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; -import { OutputChannelUpdateMode } from 'vs/workbench/contrib/output/common/output'; +import { OutputChannelUpdateMode } from 'vs/workbench/services/output/common/output'; import { InputValidationType } from 'vs/workbench/contrib/scm/common/scm'; import { IWorkspaceSymbol } from 'vs/workbench/contrib/search/common/search'; import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { CoverageDetails, ExtensionRunTestsRequest, IFileCoverage, ISerializedTestResults, ITestItem, ITestMessage, ITestRunProfile, ITestRunTask, ResolvedTestRunRequest, RunTestForControllerRequest, TestResultState, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testTypes'; -import { InternalTimelineOptions, Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor } from 'vs/workbench/contrib/timeline/common/timeline'; +import { Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor } from 'vs/workbench/contrib/timeline/common/timeline'; import { TypeHierarchyItem } from 'vs/workbench/contrib/typeHierarchy/common/typeHierarchy'; import { AuthenticationProviderInformation, AuthenticationSession, AuthenticationSessionsChangeEvent } from 'vs/workbench/services/authentication/common/authentication'; import { EditorGroupColumn } from 'vs/workbench/services/editor/common/editorGroupColumn'; -import { IStaticWorkspaceData } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; +import { IExtensionDescriptionDelta, IStaticWorkspaceData } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { IResolveAuthorityResult } from 'vs/workbench/services/extensions/common/extensionHostProxy'; import { ActivationKind, ExtensionActivationReason, MissingExtensionDependency } from 'vs/workbench/services/extensions/common/extensions'; import { createProxyIdentifier, Dto, IRPCProtocol, SerializableObjectWithBuffers } from 'vs/workbench/services/extensions/common/proxyIdentifier'; @@ -257,7 +257,7 @@ export interface MainThreadTextEditorsShape extends IDisposable { } export interface MainThreadTreeViewsShape extends IDisposable { - $registerTreeViewDataProvider(treeViewId: string, options: { showCollapseAll: boolean; canSelectMany: boolean; dropMimeTypes: string[]; dragMimeTypes: string[]; hasHandleDrag: boolean; hasHandleDrop: boolean }): Promise<void>; + $registerTreeViewDataProvider(treeViewId: string, options: { showCollapseAll: boolean; canSelectMany: boolean; dropMimeTypes: readonly string[]; dragMimeTypes: readonly string[]; hasHandleDrag: boolean; hasHandleDrop: boolean }): Promise<void>; $refresh(treeViewId: string, itemsToRefresh?: { [treeItemHandle: string]: ITreeItem }): Promise<void>; $reveal(treeViewId: string, itemInfo: { item: ITreeItem; parentChain: ITreeItem[] } | undefined, options: IRevealOptions): Promise<void>; $setMessage(treeViewId: string, message: string): void; @@ -378,7 +378,7 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $registerDocumentSemanticTokensProvider(handle: number, selector: IDocumentFilterDto[], legend: languages.SemanticTokensLegend, eventHandle: number | undefined): void; $emitDocumentSemanticTokensEvent(eventHandle: number): void; $registerDocumentRangeSemanticTokensProvider(handle: number, selector: IDocumentFilterDto[], legend: languages.SemanticTokensLegend): void; - $registerSuggestSupport(handle: number, selector: IDocumentFilterDto[], triggerCharacters: string[], supportsResolveDetails: boolean, displayName: string): void; + $registerCompletionsProvider(handle: number, selector: IDocumentFilterDto[], triggerCharacters: string[], supportsResolveDetails: boolean, displayName: string): void; $registerInlineCompletionsSupport(handle: number, selector: IDocumentFilterDto[]): void; $registerSignatureHelpProvider(handle: number, selector: IDocumentFilterDto[], metadata: ISignatureHelpProviderMetadataDto): void; $registerInlayHintsProvider(handle: number, selector: IDocumentFilterDto[], supportsResolve: boolean, eventHandle: number | undefined, displayName: string | undefined): void; @@ -625,7 +625,8 @@ export const enum TabInputKind { export const enum TabModelOperationKind { TAB_OPEN, TAB_CLOSE, - TAB_UPDATE + TAB_UPDATE, + TAB_MOVE } export interface UnknownInputDto { @@ -677,6 +678,7 @@ export interface MainThreadEditorTabsShape extends IDisposable { // manage tabs: move, close, rearrange etc $moveTab(tabId: string, index: number, viewColumn: EditorGroupColumn, preserveFocus?: boolean): void; $closeTab(tabIds: string[], preserveFocus?: boolean): Promise<boolean>; + $closeGroup(groupIds: number[], preservceFocus?: boolean): Promise<boolean>; } export interface IEditorTabGroupDto { @@ -689,11 +691,12 @@ export interface IEditorTabGroupDto { } export interface TabOperation { - readonly kind: TabModelOperationKind.TAB_OPEN | TabModelOperationKind.TAB_CLOSE | TabModelOperationKind.TAB_UPDATE; + readonly kind: TabModelOperationKind.TAB_OPEN | TabModelOperationKind.TAB_CLOSE | TabModelOperationKind.TAB_UPDATE | TabModelOperationKind.TAB_MOVE; // TODO @lramos15 Possibly get rid of index for tab update, it's only needed for open and close readonly index: number; readonly tabDto: IEditorTabDto; readonly groupId: number; + readonly oldIndex?: number; } export interface IEditorTabDto { @@ -865,6 +868,7 @@ export interface ExtHostWebviewPanelsShape { state: any; webviewOptions: IWebviewContentOptions; panelOptions: IWebviewPanelOptions; + active: boolean; }, position: EditorGroupColumn, ): Promise<void>; @@ -978,6 +982,17 @@ export interface INotebookKernelDto2 { preloads?: { uri: UriComponents; provides: string[] }[]; } +export interface INotebookProxyKernelDto { + id: string; + notebookType: string; + extensionId: ExtensionIdentifier; + extensionLocation: UriComponents; + label: string; + detail?: string; + description?: string; + kind?: string; +} + export interface ICellExecuteOutputEditDto { editType: CellExecutionUpdateType.Output; append?: boolean; @@ -1011,6 +1026,12 @@ export interface MainThreadNotebookKernelsShape extends IDisposable { $completeExecution(handle: number, data: SerializableObjectWithBuffers<ICellExecutionCompleteDto>): void; } +export interface MainThreadNotebookProxyKernelsShape extends IDisposable { + $addProxyKernel(handle: number, data: INotebookProxyKernelDto): Promise<void>; + $updateProxyKernel(handle: number, data: Partial<INotebookProxyKernelDto>): void; + $removeProxyKernel(handle: number): void; +} + export interface MainThreadNotebookRenderersShape extends IDisposable { $postMessage(editorId: string | undefined, rendererId: string, message: unknown): Promise<boolean>; } @@ -1105,6 +1126,7 @@ export interface MainThreadTaskShape extends IDisposable { } export interface MainThreadExtensionServiceShape extends IDisposable { + $getExtension(extensionId: string): Promise<Dto<IExtensionDescription> | undefined>; $activateExtension(extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise<void>; $onWillActivateExtension(extensionId: ExtensionIdentifier): Promise<void>; $onDidActivateExtension(extensionId: ExtensionIdentifier, codeLoadingTime: number, activateCallTime: number, activateResolvedTime: number, activationReason: ExtensionActivationReason): void; @@ -1412,7 +1434,7 @@ export interface ExtHostExtensionServiceShape { * Returns `null` if no resolver for `remoteAuthority` is found. */ $getCanonicalURI(remoteAuthority: string, uri: UriComponents): Promise<UriComponents | null>; - $startExtensionHost(enabledExtensionIds: ExtensionIdentifier[]): Promise<void>; + $startExtensionHost(extensionsDelta: IExtensionDescriptionDelta): Promise<void>; $extensionTestsExecute(): Promise<number>; $extensionTestsExit(code: number): Promise<void>; $activateByEvent(activationEvent: string, activationKind: ActivationKind): Promise<void>; @@ -1420,7 +1442,7 @@ export interface ExtHostExtensionServiceShape { $setRemoteEnvironment(env: { [key: string]: string | null }): Promise<void>; $updateRemoteConnectionData(connectionData: IRemoteConnectionData): Promise<void>; - $deltaExtensions(toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[]): Promise<void>; + $deltaExtensions(extensionsDelta: IExtensionDescriptionDelta): Promise<void>; $test_latency(n: number): Promise<number>; $test_up(b: VSBuffer): Promise<number>; @@ -2098,6 +2120,10 @@ export interface ExtHostNotebookKernelsShape { $cellExecutionChanged(uri: UriComponents, cellHandle: number, state: notebookCommon.NotebookCellExecutionState | undefined): void; } +export interface ExtHostNotebookProxyKernelsShape { + $resolveKernel(handle: number): Promise<string | null>; +} + export interface ExtHostInteractiveShape { $willAddInteractiveDocument(uri: UriComponents, eol: string, languageId: string, notebookUri: UriComponents): void; $willRemoveInteractiveDocument(uri: UriComponents, notebookUri: UriComponents): void; @@ -2133,7 +2159,7 @@ export interface ExtHostTunnelServiceShape { } export interface ExtHostTimelineShape { - $getTimeline(source: string, uri: UriComponents, options: TimelineOptions, token: CancellationToken, internalOptions?: InternalTimelineOptions): Promise<Dto<Timeline> | undefined>; + $getTimeline(source: string, uri: UriComponents, options: TimelineOptions, token: CancellationToken): Promise<Dto<Timeline> | undefined>; } export const enum ExtHostTestingResource { @@ -2274,6 +2300,7 @@ export const MainContext = { MainThreadNotebookDocuments: createProxyIdentifier<MainThreadNotebookDocumentsShape>('MainThreadNotebookDocumentsShape'), MainThreadNotebookEditors: createProxyIdentifier<MainThreadNotebookEditorsShape>('MainThreadNotebookEditorsShape'), MainThreadNotebookKernels: createProxyIdentifier<MainThreadNotebookKernelsShape>('MainThreadNotebookKernels'), + MainThreadNotebookProxyKernels: createProxyIdentifier<MainThreadNotebookProxyKernelsShape>('MainThreadNotebookProxyKernels'), MainThreadNotebookRenderers: createProxyIdentifier<MainThreadNotebookRenderersShape>('MainThreadNotebookRenderers'), MainThreadInteractive: createProxyIdentifier<MainThreadInteractiveShape>('MainThreadInteractive'), MainThreadTheming: createProxyIdentifier<MainThreadThemingShape>('MainThreadTheming'), @@ -2326,6 +2353,7 @@ export const ExtHostContext = { ExtHostNotebookDocuments: createProxyIdentifier<ExtHostNotebookDocumentsShape>('ExtHostNotebookDocuments'), ExtHostNotebookEditors: createProxyIdentifier<ExtHostNotebookEditorsShape>('ExtHostNotebookEditors'), ExtHostNotebookKernels: createProxyIdentifier<ExtHostNotebookKernelsShape>('ExtHostNotebookKernels'), + ExtHostNotebookProxyKernels: createProxyIdentifier<ExtHostNotebookProxyKernelsShape>('ExtHostNotebookProxyKernels'), ExtHostNotebookRenderers: createProxyIdentifier<ExtHostNotebookRenderersShape>('ExtHostNotebookRenderers'), ExtHostInteractive: createProxyIdentifier<ExtHostInteractiveShape>('ExtHostInteractive'), ExtHostTheming: createProxyIdentifier<ExtHostThemingShape>('ExtHostTheming'), diff --git a/src/vs/workbench/api/common/extHostApiDeprecationService.ts b/src/vs/workbench/api/common/extHostApiDeprecationService.ts index a407b241c33..59841770307 100644 --- a/src/vs/workbench/api/common/extHostApiDeprecationService.ts +++ b/src/vs/workbench/api/common/extHostApiDeprecationService.ts @@ -47,8 +47,10 @@ export class ExtHostApiDeprecationService implements IExtHostApiDeprecationServi apiId: string; }; type DeprecationTelemetryMeta = { - extensionId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; owner: 'mjbvz'; comment: 'The id of the extension that is using the deprecated API' }; - apiId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; owner: 'mjbvz'; comment: 'The id of the deprecated API' }; + extensionId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The id of the extension that is using the deprecated API' }; + apiId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The id of the deprecated API' }; + owner: 'mjbvz'; + comment: 'Helps us gain insights on extensions using deprecated API so we can assist in migration to new API'; }; this._telemetryShape.$publicLog2<DeprecationTelemetry, DeprecationTelemetryMeta>('extHostDeprecatedApiUsage', { extensionId: extension.identifier.value, diff --git a/src/vs/workbench/api/common/extHostCommands.ts b/src/vs/workbench/api/common/extHostCommands.ts index c8fb9cf0ed6..d0ae8f53ff4 100644 --- a/src/vs/workbench/api/common/extHostCommands.ts +++ b/src/vs/workbench/api/common/extHostCommands.ts @@ -8,7 +8,7 @@ import { ICommandHandlerDescription } from 'vs/platform/commands/common/commands import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; import * as extHostTypeConverter from 'vs/workbench/api/common/extHostTypeConverters'; import { cloneAndChange } from 'vs/base/common/objects'; -import { MainContext, MainThreadCommandsShape, ExtHostCommandsShape, ICommandDto, ICommandHandlerDescriptionDto } from './extHost.protocol'; +import { MainContext, MainThreadCommandsShape, ExtHostCommandsShape, ICommandDto, ICommandHandlerDescriptionDto, MainThreadTelemetryShape } from './extHost.protocol'; import { isNonEmptyArray } from 'vs/base/common/arrays'; import * as languages from 'vs/editor/common/languages'; import type * as vscode from 'vscode'; @@ -46,6 +46,7 @@ export class ExtHostCommands implements ExtHostCommandsShape { private readonly _commands = new Map<string, CommandHandler>(); private readonly _apiCommands = new Map<string, ApiCommand>(); + #telemetry: MainThreadTelemetryShape; private readonly _logService: ILogService; private readonly _argumentProcessors: ArgumentProcessor[]; @@ -58,6 +59,7 @@ export class ExtHostCommands implements ExtHostCommandsShape { ) { this.#proxy = extHostRpc.getProxy(MainContext.MainThreadCommands); this._logService = logService; + this.#telemetry = extHostRpc.getProxy(MainContext.MainThreadTelemetry); this.converter = new CommandsConverter( this, id => { @@ -219,6 +221,7 @@ export class ExtHostCommands implements ExtHostCommandsShape { if (!command) { throw new Error('Unknown command'); } + this._reportTelemetry(command, id); let { callback, thisArg, description } = command; if (description) { for (let i = 0; i < description.args.length; i++) { @@ -257,6 +260,26 @@ export class ExtHostCommands implements ExtHostCommandsShape { } } + private _reportTelemetry(command: CommandHandler, id: string) { + if (!command.extension || command.extension.isBuiltin) { + return; + } + type ExtensionActionTelemetry = { + extensionId: string; + id: string; + }; + type ExtensionActionTelemetryMeta = { + extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The id of the extension handling the command, informing which extensions provide most-used functionality.' }; + id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The id of the command, to understand which specific extension features are most popular.' }; + owner: 'digitarald'; + comment: 'Used to gain insight on the most popular commands used from extensions'; + }; + this.#telemetry.$publicLog2<ExtensionActionTelemetry, ExtensionActionTelemetryMeta>('Extension:ActionExecuted', { + extensionId: command.extension.identifier.value, + id: id, + }); + } + $executeContributedCommand(id: string, ...args: any[]): Promise<unknown> { this._logService.trace('ExtHostCommands#$executeContributedCommand', id); diff --git a/src/vs/workbench/api/common/extHostCustomEditors.ts b/src/vs/workbench/api/common/extHostCustomEditors.ts index 7c9e25f6126..24ee34186c0 100644 --- a/src/vs/workbench/api/common/extHostCustomEditors.ts +++ b/src/vs/workbench/api/common/extHostCustomEditors.ts @@ -267,7 +267,7 @@ export class ExtHostCustomEditors implements extHostProtocol.ExtHostCustomEditor const viewColumn = typeConverters.ViewColumn.to(position); const webview = this._extHostWebview.createNewWebview(handle, initData.webviewOptions, entry.extension); - const panel = this._extHostWebviewPanels.createNewWebviewPanel(handle, viewType, initData.title, viewColumn, initData.panelOptions, webview); + const panel = this._extHostWebviewPanels.createNewWebviewPanel(handle, viewType, initData.title, viewColumn, initData.panelOptions, webview, true); const revivedResource = URI.revive(resource); diff --git a/src/vs/workbench/api/common/extHostDebugService.ts b/src/vs/workbench/api/common/extHostDebugService.ts index 228920010ae..178ab59c5ef 100644 --- a/src/vs/workbench/api/common/extHostDebugService.ts +++ b/src/vs/workbench/api/common/extHostDebugService.ts @@ -3,36 +3,29 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as path from 'vs/base/common/path'; -import { URI, UriComponents } from 'vs/base/common/uri'; -import { Event, Emitter } from 'vs/base/common/event'; import { asPromise } from 'vs/base/common/async'; -import { - MainContext, MainThreadDebugServiceShape, ExtHostDebugServiceShape, DebugSessionUUID, - IBreakpointsDeltaDto, ISourceMultiBreakpointDto, IFunctionBreakpointDto, IDebugSessionDto -} from 'vs/workbench/api/common/extHost.protocol'; -import { Disposable, Position, Location, SourceBreakpoint, FunctionBreakpoint, DebugAdapterServer, DebugAdapterExecutable, DataBreakpoint, DebugConsoleMode, DebugAdapterInlineImplementation, DebugAdapterNamedPipeServer, TextDiffTabInput, NotebookDiffEditorTabInput, TextTabInput, NotebookEditorTabInput, CustomEditorTabInput } from 'vs/workbench/api/common/extHostTypes'; -import { AbstractDebugAdapter } from 'vs/workbench/contrib/debug/common/abstractDebugAdapter'; -import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; -import { IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; -import { ExtHostDocumentsAndEditors, IExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; -import { IDebuggerContribution, IConfig, IDebugAdapter, IDebugAdapterServer, IDebugAdapterExecutable, IAdapterDescriptor, IDebugAdapterNamedPipeServer } from 'vs/workbench/contrib/debug/common/debug'; -import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { AbstractVariableResolverService } from 'vs/workbench/services/configurationResolver/common/variableResolver'; -import { ExtHostConfigProvider, IExtHostConfiguration } from '../common/extHostConfiguration'; -import { convertToVSCPaths, convertToDAPaths, isDebuggerMainContribution } from 'vs/workbench/contrib/debug/common/debugUtils'; -import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; -import { ISignService } from 'vs/platform/sign/common/sign'; -import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; -import type * as vscode from 'vscode'; +import { Emitter, Event } from 'vs/base/common/event'; +import { withNullAsUndefined } from 'vs/base/common/types'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { withNullAsUndefined } from 'vs/base/common/types'; -import * as process from 'vs/base/common/process'; +import { ISignService } from 'vs/platform/sign/common/sign'; +import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { DebugSessionUUID, ExtHostDebugServiceShape, IBreakpointsDeltaDto, IDebugSessionDto, IFunctionBreakpointDto, ISourceMultiBreakpointDto, MainContext, MainThreadDebugServiceShape } from 'vs/workbench/api/common/extHost.protocol'; import { IExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs'; +import { IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; +import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; +import { DataBreakpoint, DebugAdapterExecutable, DebugAdapterInlineImplementation, DebugAdapterNamedPipeServer, DebugAdapterServer, DebugConsoleMode, Disposable, FunctionBreakpoint, Location, Position, SourceBreakpoint } from 'vs/workbench/api/common/extHostTypes'; +import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; +import { AbstractDebugAdapter } from 'vs/workbench/contrib/debug/common/abstractDebugAdapter'; +import { IAdapterDescriptor, IConfig, IDebugAdapter, IDebugAdapterExecutable, IDebugAdapterNamedPipeServer, IDebugAdapterServer, IDebuggerContribution } from 'vs/workbench/contrib/debug/common/debug'; +import { convertToDAPaths, convertToVSCPaths, isDebuggerMainContribution } from 'vs/workbench/contrib/debug/common/debugUtils'; +import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; import { Dto } from 'vs/workbench/services/extensions/common/proxyIdentifier'; +import type * as vscode from 'vscode'; +import { IExtHostConfiguration } from '../common/extHostConfiguration'; +import { IExtHostVariableResolverProvider } from './extHostVariableResolverService'; export const IExtHostDebugService = createDecorator<IExtHostDebugService>('IExtHostDebugService'); @@ -101,17 +94,15 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E private _debugAdapters: Map<number, IDebugAdapter>; private _debugAdaptersTrackers: Map<number, vscode.DebugAdapterTracker>; - private _variableResolver: IConfigurationResolverService | undefined; - private _signService: ISignService | undefined; constructor( @IExtHostRpcService extHostRpcService: IExtHostRpcService, @IExtHostWorkspace protected _workspaceService: IExtHostWorkspace, @IExtHostExtensionService private _extensionService: IExtHostExtensionService, - @IExtHostDocumentsAndEditors private _editorsService: IExtHostDocumentsAndEditors, @IExtHostConfiguration protected _configurationService: IExtHostConfiguration, - @IExtHostEditorTabs protected _editorTabs: IExtHostEditorTabs + @IExtHostEditorTabs protected _editorTabs: IExtHostEditorTabs, + @IExtHostVariableResolverProvider private _variableResolver: IExtHostVariableResolverProvider, ) { this._configProviderHandleCounter = 0; this._configProviders = []; @@ -371,13 +362,7 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E return Promise.resolve(undefined); } - protected abstract createVariableResolver(folders: vscode.WorkspaceFolder[], editorService: ExtHostDocumentsAndEditors, configurationService: ExtHostConfigProvider): AbstractVariableResolverService; - public async $substituteVariables(folderUri: UriComponents | undefined, config: IConfig): Promise<IConfig> { - if (!this._variableResolver) { - const [workspaceFolders, configProvider] = await Promise.all([this._workspaceService.getWorkspaceFolders2(), this._configurationService.getConfigProvider()]); - this._variableResolver = this.createVariableResolver(workspaceFolders || [], this._editorsService, configProvider!); - } let ws: IWorkspaceFolder | undefined; const folder = await this.getFolder(folderUri); if (folder) { @@ -390,7 +375,8 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E } }; } - return this._variableResolver.resolveAnyAsync(ws, config); + const variableResolver = await this._variableResolver.getResolver(); + return variableResolver.resolveAnyAsync(ws, config); } protected createDebugAdapter(adapter: IAdapterDescriptor, session: ExtHostDebugSession): AbstractDebugAdapter | undefined { @@ -939,94 +925,6 @@ export class ExtHostDebugConsole { } } -export class ExtHostVariableResolverService extends AbstractVariableResolverService { - - constructor(folders: vscode.WorkspaceFolder[], - editorService: ExtHostDocumentsAndEditors | undefined, - configurationService: ExtHostConfigProvider, - editorTabs: IExtHostEditorTabs, - workspaceService?: IExtHostWorkspace, - userHome?: string) { - function getActiveUri(): URI | undefined { - if (editorService) { - const activeEditor = editorService.activeEditor(); - if (activeEditor) { - return activeEditor.document.uri; - } - const activeTab = editorTabs.tabGroups.groups.find(group => group.isActive)?.activeTab; - if (activeTab !== undefined) { - // Resolve a resource from the tab - if (activeTab.kind instanceof TextDiffTabInput || activeTab.kind instanceof NotebookDiffEditorTabInput) { - return activeTab.kind.modified; - } else if (activeTab.kind instanceof TextTabInput || activeTab.kind instanceof NotebookEditorTabInput || activeTab.kind instanceof CustomEditorTabInput) { - return activeTab.kind.uri; - } - } - } - return undefined; - } - - super({ - getFolderUri: (folderName: string): URI | undefined => { - const found = folders.filter(f => f.name === folderName); - if (found && found.length > 0) { - return found[0].uri; - } - return undefined; - }, - getWorkspaceFolderCount: (): number => { - return folders.length; - }, - getConfigurationValue: (folderUri: URI | undefined, section: string): string | undefined => { - return configurationService.getConfiguration(undefined, folderUri).get<string>(section); - }, - getAppRoot: (): string | undefined => { - return process.cwd(); - }, - getExecPath: (): string | undefined => { - return process.env['VSCODE_EXEC_PATH']; - }, - getFilePath: (): string | undefined => { - const activeUri = getActiveUri(); - if (activeUri) { - return path.normalize(activeUri.fsPath); - } - return undefined; - }, - getWorkspaceFolderPathForFile: (): string | undefined => { - if (workspaceService) { - const activeUri = getActiveUri(); - if (activeUri) { - const ws = workspaceService.getWorkspaceFolder(activeUri); - if (ws) { - return path.normalize(ws.uri.fsPath); - } - } - } - return undefined; - }, - getSelectedText: (): string | undefined => { - if (editorService) { - const activeEditor = editorService.activeEditor(); - if (activeEditor && !activeEditor.selection.isEmpty) { - return activeEditor.document.getText(activeEditor.selection); - } - } - return undefined; - }, - getLineNumber: (): string | undefined => { - if (editorService) { - const activeEditor = editorService.activeEditor(); - if (activeEditor) { - return String(activeEditor.selection.end.line + 1); - } - } - return undefined; - } - }, undefined, userHome ? Promise.resolve(userHome) : undefined, Promise.resolve(process.env)); - } -} - interface ConfigProviderTuple { type: string; handle: number; @@ -1108,14 +1006,10 @@ export class WorkerExtHostDebugService extends ExtHostDebugServiceBase { @IExtHostRpcService extHostRpcService: IExtHostRpcService, @IExtHostWorkspace workspaceService: IExtHostWorkspace, @IExtHostExtensionService extensionService: IExtHostExtensionService, - @IExtHostDocumentsAndEditors editorsService: IExtHostDocumentsAndEditors, @IExtHostConfiguration configurationService: IExtHostConfiguration, - @IExtHostEditorTabs editorTabs: IExtHostEditorTabs + @IExtHostEditorTabs editorTabs: IExtHostEditorTabs, + @IExtHostVariableResolverProvider variableResolver: IExtHostVariableResolverProvider ) { - super(extHostRpcService, workspaceService, extensionService, editorsService, configurationService, editorTabs); - } - - protected createVariableResolver(folders: vscode.WorkspaceFolder[], editorService: ExtHostDocumentsAndEditors, configurationService: ExtHostConfigProvider): AbstractVariableResolverService { - return new ExtHostVariableResolverService(folders, editorService, configurationService, this._editorTabs); + super(extHostRpcService, workspaceService, extensionService, configurationService, editorTabs, variableResolver); } } diff --git a/src/vs/workbench/api/common/extHostEditorTabs.ts b/src/vs/workbench/api/common/extHostEditorTabs.ts index cbd2effae0c..1fbc2d487de 100644 --- a/src/vs/workbench/api/common/extHostEditorTabs.ts +++ b/src/vs/workbench/api/common/extHostEditorTabs.ts @@ -9,9 +9,10 @@ import { IEditorTabDto, IEditorTabGroupDto, IExtHostEditorTabsShape, MainContext import { URI } from 'vs/base/common/uri'; import { Emitter } from 'vs/base/common/event'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { CustomEditorTabInput, NotebookDiffEditorTabInput, NotebookEditorTabInput, TerminalEditorTabInput, TextDiffTabInput, TextTabInput, ViewColumn, WebviewEditorTabInput } from 'vs/workbench/api/common/extHostTypes'; +import { CustomEditorTabInput, NotebookDiffEditorTabInput, NotebookEditorTabInput, TerminalEditorTabInput, TextDiffTabInput, TextTabInput, WebviewEditorTabInput } from 'vs/workbench/api/common/extHostTypes'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { assertIsDefined } from 'vs/base/common/types'; +import { diffSets } from 'vs/base/common/collections'; export interface IExtHostEditorTabs extends IExtHostEditorTabsShape { readonly _serviceBrand: undefined; @@ -47,7 +48,7 @@ class ExtHostEditorTab { get label() { return that._dto.label; }, - get kind() { + get input() { return that._input; }, get isDirty() { @@ -174,6 +175,17 @@ class ExtHostEditorTabGroup { this._activeTabId = ''; } return tab; + } else if (operation.kind === TabModelOperationKind.TAB_MOVE) { + if (operation.oldIndex === undefined) { + throw new Error('Invalid old index on move IPC'); + } + // Splice to remove at old index and insert at new index === moving the tab + const tab = this._tabs.splice(operation.oldIndex, 1)[0]; + if (!tab) { + throw new Error(`Tab move updated received for index ${operation.oldIndex} which does not exist`); + } + this._tabs.splice(operation.index, 0, tab); + return tab; } const tab = this._tabs.find(extHostTab => extHostTab.tabId === operation.tabDto.id); if (!tab) { @@ -201,8 +213,8 @@ export class ExtHostEditorTabs implements IExtHostEditorTabs { readonly _serviceBrand: undefined; private readonly _proxy: MainThreadEditorTabsShape; - private readonly _onDidChangeTabs = new Emitter<vscode.Tab[]>(); - private readonly _onDidChangeTabGroups = new Emitter<vscode.TabGroup[]>(); + private readonly _onDidChangeTabs = new Emitter<vscode.TabChangeEvent>(); + private readonly _onDidChangeTabGroups = new Emitter<vscode.TabGroupChangeEvent>(); // Have to use ! because this gets initialized via an RPC proxy private _activeGroupId!: number; @@ -223,7 +235,7 @@ export class ExtHostEditorTabs implements IExtHostEditorTabs { onDidChangeTabGroups: that._onDidChangeTabGroups.event, onDidChangeTabs: that._onDidChangeTabs.event, // dynamic -> getters - get groups() { + get all() { return Object.freeze(that._extHostTabGroups.map(group => group.apiObject)); }, get activeTabGroup() { @@ -231,47 +243,51 @@ export class ExtHostEditorTabs implements IExtHostEditorTabs { const activeTabGroup = assertIsDefined(that._extHostTabGroups.find(candidate => candidate.groupId === activeTabGroupId)?.apiObject); return activeTabGroup; }, - close: async (tab: vscode.Tab | vscode.Tab[], preserveFocus?: boolean) => { - const tabs = Array.isArray(tab) ? tab : [tab]; - const extHostTabIds: string[] = []; - for (const tab of tabs) { - const extHostTab = this._findExtHostTabFromApi(tab); - if (!extHostTab) { - throw new Error('Tab close: Invalid tab not found!'); - } - extHostTabIds.push(extHostTab.tabId); + close: async (tabOrTabGroup: vscode.Tab | readonly vscode.Tab[] | vscode.TabGroup | readonly vscode.TabGroup[], preserveFocus?: boolean) => { + const tabsOrTabGroups = Array.isArray(tabOrTabGroup) ? tabOrTabGroup : [tabOrTabGroup]; + if (!tabsOrTabGroups.length) { + return true; } - return this._proxy.$closeTab(extHostTabIds, preserveFocus); - }, - move: async (tab: vscode.Tab, viewColumn: ViewColumn, index: number, preservceFocus?: boolean) => { - const extHostTab = this._findExtHostTabFromApi(tab); - if (!extHostTab) { - throw new Error('Invalid tab'); + // Check which type was passed in and call the appropriate close + // Casting is needed as typescript doesn't seem to infer enough from this + if (isTabGroup(tabsOrTabGroups[0])) { + return this._closeGroups(tabsOrTabGroups as vscode.TabGroup[], preserveFocus); + } else { + return this._closeTabs(tabsOrTabGroups as vscode.Tab[], preserveFocus); } - this._proxy.$moveTab(extHostTab.tabId, index, typeConverters.ViewColumn.from(viewColumn), preservceFocus); - return; - } + }, + // move: async (tab: vscode.Tab, viewColumn: ViewColumn, index: number, preservceFocus?: boolean) => { + // const extHostTab = this._findExtHostTabFromApi(tab); + // if (!extHostTab) { + // throw new Error('Invalid tab'); + // } + // this._proxy.$moveTab(extHostTab.tabId, index, typeConverters.ViewColumn.from(viewColumn), preservceFocus); + // return; + // } }; this._apiObject = Object.freeze(obj); } return this._apiObject; } - private _findExtHostTabFromApi(apiTab: vscode.Tab): ExtHostEditorTab | undefined { - for (const group of this._extHostTabGroups) { - for (const tab of group.tabs) { - if (tab.apiObject === apiTab) { - return tab; - } - } - } - return; - } - $acceptEditorTabModel(tabGroups: IEditorTabGroupDto[]): void { + const groupIdsBefore = new Set(this._extHostTabGroups.map(group => group.groupId)); + const groupIdsAfter = new Set(tabGroups.map(dto => dto.groupId)); + const diff = diffSets(groupIdsBefore, groupIdsAfter); + + const closed: vscode.TabGroup[] = this._extHostTabGroups.filter(group => diff.removed.includes(group.groupId)).map(group => group.apiObject); + const opened: vscode.TabGroup[] = []; + const changed: vscode.TabGroup[] = []; + + this._extHostTabGroups = tabGroups.map(tabGroup => { const group = new ExtHostEditorTabGroup(tabGroup, this._proxy, () => this._activeGroupId); + if (diff.added.includes(group.groupId)) { + opened.push(group.apiObject); + } else { + changed.push(group.apiObject); + } return group; }); @@ -280,7 +296,7 @@ export class ExtHostEditorTabs implements IExtHostEditorTabs { if (activeTabGroupId !== undefined && this._activeGroupId !== activeTabGroupId) { this._activeGroupId = activeTabGroupId; } - this._onDidChangeTabGroups.fire(this._extHostTabGroups.map(g => g.apiObject)); + this._onDidChangeTabGroups.fire(Object.freeze({ opened, closed, changed })); } $acceptTabGroupUpdate(groupDto: IEditorTabGroupDto) { @@ -292,7 +308,7 @@ export class ExtHostEditorTabs implements IExtHostEditorTabs { if (groupDto.isActive) { this._activeGroupId = groupDto.groupId; } - this._onDidChangeTabGroups.fire([group.apiObject]); + this._onDidChangeTabGroups.fire(Object.freeze({ changed: [group.apiObject], opened: [], closed: [] })); } $acceptTabOperation(operation: TabOperation) { @@ -301,9 +317,80 @@ export class ExtHostEditorTabs implements IExtHostEditorTabs { throw new Error('Update Tabs IPC call received before group creation.'); } const tab = group.acceptTabOperation(operation); - // We don't want to fire a change event with a closed tab to prevent an invalid tabs from being received - if (operation.kind !== TabModelOperationKind.TAB_CLOSE) { - this._onDidChangeTabs.fire([tab.apiObject]); + + // Construct the tab change event based on the operation + switch (operation.kind) { + case TabModelOperationKind.TAB_OPEN: + this._onDidChangeTabs.fire(Object.freeze({ + opened: [tab.apiObject], + closed: [], + changed: [] + })); + return; + case TabModelOperationKind.TAB_CLOSE: + this._onDidChangeTabs.fire(Object.freeze({ + opened: [], + closed: [tab.apiObject], + changed: [] + })); + return; + case TabModelOperationKind.TAB_MOVE: + case TabModelOperationKind.TAB_UPDATE: + this._onDidChangeTabs.fire(Object.freeze({ + opened: [], + closed: [], + changed: [tab.apiObject] + })); + return; } } + + private _findExtHostTabFromApi(apiTab: vscode.Tab): ExtHostEditorTab | undefined { + for (const group of this._extHostTabGroups) { + for (const tab of group.tabs) { + if (tab.apiObject === apiTab) { + return tab; + } + } + } + return; + } + + private _findExtHostTabGroupFromApi(apiTabGroup: vscode.TabGroup): ExtHostEditorTabGroup | undefined { + return this._extHostTabGroups.find(candidate => candidate.apiObject === apiTabGroup); + } + + private async _closeTabs(tabs: vscode.Tab[], preserveFocus?: boolean): Promise<boolean> { + const extHostTabIds: string[] = []; + for (const tab of tabs) { + const extHostTab = this._findExtHostTabFromApi(tab); + if (!extHostTab) { + throw new Error('Tab close: Invalid tab not found!'); + } + extHostTabIds.push(extHostTab.tabId); + } + return this._proxy.$closeTab(extHostTabIds, preserveFocus); + } + + private async _closeGroups(groups: vscode.TabGroup[], preserverFoucs?: boolean): Promise<boolean> { + const extHostGroupIds: number[] = []; + for (const group of groups) { + const extHostGroup = this._findExtHostTabGroupFromApi(group); + if (!extHostGroup) { + throw new Error('Group close: Invalid group not found!'); + } + extHostGroupIds.push(extHostGroup.groupId); + } + return this._proxy.$closeGroup(extHostGroupIds, preserverFoucs); + } +} + +//#region Utils +function isTabGroup(obj: unknown): obj is vscode.TabGroup { + const tabGroup = obj as vscode.TabGroup; + if (tabGroup.tabs !== undefined) { + return true; + } + return false; } +//#endregion diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts index b3ff5c0c2b4..49eaa65d462 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -13,12 +13,12 @@ import { TernarySearchTree } from 'vs/base/common/map'; import { URI, UriComponents } from 'vs/base/common/uri'; import { ILogService } from 'vs/platform/log/common/log'; import { ExtHostExtensionServiceShape, MainContext, MainThreadExtensionServiceShape, MainThreadTelemetryShape, MainThreadWorkspaceShape } from 'vs/workbench/api/common/extHost.protocol'; -import { IExtensionHostInitData } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; +import { IExtensionDescriptionDelta, IExtensionHostInitData } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { ExtHostConfiguration, IExtHostConfiguration } from 'vs/workbench/api/common/extHostConfiguration'; import { ActivatedExtension, EmptyExtension, ExtensionActivationTimes, ExtensionActivationTimesBuilder, ExtensionsActivator, IExtensionAPI, IExtensionModule, HostExtension, ExtensionActivationTimesFragment } from 'vs/workbench/api/common/extHostExtensionActivator'; import { ExtHostStorage, IExtHostStorage } from 'vs/workbench/api/common/extHostStorage'; import { ExtHostWorkspace, IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; -import { MissingExtensionDependency, ActivationKind, checkProposedApiEnabled, isProposedApiEnabled, ExtensionActivationReason } from 'vs/workbench/services/extensions/common/extensions'; +import { MissingExtensionDependency, ActivationKind, checkProposedApiEnabled, isProposedApiEnabled, ExtensionActivationReason, extensionIdentifiersArrayToSet } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; import * as errors from 'vs/base/common/errors'; import type * as vscode from 'vscode'; @@ -100,16 +100,18 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme private readonly _readyToRunExtensions: Barrier; private readonly _eagerExtensionsActivated: Barrier; - protected readonly _registry: ExtensionDescriptionRegistry; + protected readonly _myRegistry: ExtensionDescriptionRegistry; + protected readonly _globalRegistry: ExtensionDescriptionRegistry; private readonly _storage: ExtHostStorage; private readonly _secretState: ExtHostSecretState; private readonly _storagePath: IExtensionStoragePaths; private readonly _activator: ExtensionsActivator; - private _extensionPathIndex: Promise<TernarySearchTree<URI, IExtensionDescription>> | null; + private _extensionPathIndex: Promise<ExtensionPaths> | null; private readonly _resolvers: { [authorityPrefix: string]: vscode.RemoteAuthorityResolver }; private _started: boolean; + private _isTerminating: boolean = false; private _remoteConnectionData: IRemoteConnectionData | null; constructor( @@ -143,7 +145,11 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme this._readyToStartExtensionHost = new Barrier(); this._readyToRunExtensions = new Barrier(); this._eagerExtensionsActivated = new Barrier(); - this._registry = new ExtensionDescriptionRegistry(this._initData.extensions); + this._globalRegistry = new ExtensionDescriptionRegistry(this._initData.allExtensions); + const myExtensionsSet = extensionIdentifiersArrayToSet(this._initData.myExtensions); + this._myRegistry = new ExtensionDescriptionRegistry( + filterExtensions(this._globalRegistry, myExtensionsSet) + ); this._storage = new ExtHostStorage(this._extHostContext); this._secretState = new ExtHostSecretState(this._extHostContext); this._storagePath = storagePath; @@ -153,24 +159,33 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme [IExtHostSecretState, this._secretState] )); - const hostExtensions = new Set<string>(); - this._initData.hostExtensions.forEach((extensionId) => hostExtensions.add(ExtensionIdentifier.toKey(extensionId))); + let resolvedExtensions: ExtensionIdentifier[] = []; + let hostExtensions: ExtensionIdentifier[] = []; + if (this._initData.remote.isRemote) { + resolvedExtensions = this._initData.allExtensions.filter(extension => !extension.main && !extension.browser).map(extension => extension.identifier); + hostExtensions = ( + this._initData.allExtensions + .filter(extension => !myExtensionsSet.has(ExtensionIdentifier.toKey(extension.identifier.value))) + .filter(extension => (extension.main || extension.browser) && extension.api === 'none').map(extension => extension.identifier) + ); + } + const hostExtensionsSet = extensionIdentifiersArrayToSet(hostExtensions); this._activator = this._register(new ExtensionsActivator( - this._registry, - this._initData.resolvedExtensions, - this._initData.hostExtensions, + this._myRegistry, + resolvedExtensions, + hostExtensions, { onExtensionActivationError: (extensionId: ExtensionIdentifier, error: Error, missingExtensionDependency: MissingExtensionDependency | null): void => { this._mainThreadExtensionsProxy.$onExtensionActivationError(extensionId, errors.transformErrorForSerialization(error), missingExtensionDependency); }, actualActivateExtension: async (extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise<ActivatedExtension> => { - if (hostExtensions.has(ExtensionIdentifier.toKey(extensionId))) { + if (hostExtensionsSet.has(ExtensionIdentifier.toKey(extensionId))) { await this._mainThreadExtensionsProxy.$activateExtension(extensionId, reason); return new HostExtension(); } - const extensionDescription = this._registry.getExtensionDescription(extensionId)!; + const extensionDescription = this._myRegistry.getExtensionDescription(extensionId)!; return this._activateExtension(extensionDescription, reason); } }, @@ -204,12 +219,12 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme } } - public async deactivateAll(): Promise<void> { + private async _deactivateAll(): Promise<void> { this._storagePath.onWillDeactivateAll(); let allPromises: Promise<void>[] = []; try { - const allExtensions = this._registry.getAllExtensionDescriptions(); + const allExtensions = this._myRegistry.getAllExtensionDescriptions(); const allExtensionsIds = allExtensions.map(ext => ext.identifier); const activatedExtensions = allExtensionsIds.filter(id => this.isActivated(id)); @@ -222,6 +237,40 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme await Promise.all(allPromises); } + public terminate(reason: string, code: number = 0): void { + if (this._isTerminating) { + // we are already shutting down... + return; + } + this._isTerminating = true; + this._logService.info(`Extension host terminating: ${reason}`); + this._logService.flush(); + + this._extHostTerminalService.dispose(); + this._activator.dispose(); + + errors.setUnexpectedErrorHandler((err) => { + this._logService.error(err); + }); + + // Invalidate all proxies + this._extHostContext.dispose(); + + const extensionsDeactivated = this._deactivateAll(); + + // Give extensions at most 5 seconds to wrap up any async deactivate, then exit + Promise.race([timeout(5000), extensionsDeactivated]).finally(() => { + if (this._hostUtils.pid) { + this._logService.info(`Extension host with pid ${this._hostUtils.pid} exiting with code ${code}`); + } else { + this._logService.info(`Extension host exiting with code ${code}`); + } + this._logService.flush(); + this._logService.dispose(); + this._hostUtils.exit(code); + }); + } + public isActivated(extensionId: ExtensionIdentifier): boolean { if (this._readyToRunExtensions.isOpen()) { return this._activator.isActivated(extensionId); @@ -229,6 +278,15 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme return false; } + public async getExtension(extensionId: string): Promise<IExtensionDescription | undefined> { + const ext = await this._mainThreadExtensionsProxy.$getExtension(extensionId); + return ext && { + ...ext, + identifier: new ExtensionIdentifier(ext.identifier.value), + extensionLocation: URI.revive(ext.extensionLocation), + }; + } + private _activateByEvent(activationEvent: string, startup: boolean): Promise<void> { return this._activator.activateByEvent(activationEvent, startup); } @@ -249,7 +307,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme } public getExtensionRegistry(): Promise<ExtensionDescriptionRegistry> { - return this._readyToRunExtensions.wait().then(_ => this._registry); + return this._readyToRunExtensions.wait().then(_ => this._myRegistry); } public getExtensionExports(extensionId: ExtensionIdentifier): IExtensionAPI | null | undefined { @@ -272,28 +330,35 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme } // create trie to enable fast 'filename -> extension id' look up - public async getExtensionPathIndex(): Promise<TernarySearchTree<URI, IExtensionDescription>> { + public async getExtensionPathIndex(): Promise<ExtensionPaths> { if (!this._extensionPathIndex) { - this._extensionPathIndex = (async () => { - const tst = TernarySearchTree.forUris<IExtensionDescription>(key => { - // using the default/biased extUri-util because the IExtHostFileSystemInfo-service - // isn't ready to be used yet, e.g the knowledge about `file` protocol and others - // comes in while this code runs - return extUriBiasedIgnorePathCase.ignorePathCasing(key); - }); - // const tst = TernarySearchTree.forUris<IExtensionDescription>(key => true); - for (const ext of this._registry.getAllExtensionDescriptions()) { - if (this._getEntryPoint(ext)) { - const uri = await this._realPathExtensionUri(ext.extensionLocation); - tst.set(uri, ext); - } - } - return tst; - })(); + this._extensionPathIndex = this._createExtensionPathIndex(this._myRegistry.getAllExtensionDescriptions()).then((searchTree) => { + return new ExtensionPaths(searchTree); + }); } return this._extensionPathIndex; } + /** + * create trie to enable fast 'filename -> extension id' look up + */ + private async _createExtensionPathIndex(extensions: IExtensionDescription[]): Promise<TernarySearchTree<URI, IExtensionDescription>> { + const tst = TernarySearchTree.forUris<IExtensionDescription>(key => { + // using the default/biased extUri-util because the IExtHostFileSystemInfo-service + // isn't ready to be used yet, e.g the knowledge about `file` protocol and others + // comes in while this code runs + return extUriBiasedIgnorePathCase.ignorePathCasing(key); + }); + // const tst = TernarySearchTree.forUris<IExtensionDescription>(key => true); + await Promise.all(extensions.map(async (ext) => { + if (this._getEntryPoint(ext)) { + const uri = await this._realPathExtensionUri(ext.extensionLocation); + tst.set(uri, ext); + } + })); + return tst; + } + private _deactivate(extensionId: ExtensionIdentifier): Promise<void> { let result = Promise.resolve(undefined); @@ -448,7 +513,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme get extensionMode() { return extensionMode; }, get extension() { if (extension === undefined) { - extension = new Extension(that, extensionDescription.identifier, extensionDescription, extensionKind); + extension = new Extension(that, extensionDescription.identifier, extensionDescription, extensionKind, false); } return extension; }, @@ -528,7 +593,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme // startup is considered finished this._mainThreadExtensionsProxy.$setPerformanceMarks(performance.getMarks()); - for (const desc of this._registry.getAllExtensionDescriptions()) { + for (const desc of this._myRegistry.getAllExtensionDescriptions()) { if (desc.activationEvents) { for (const activationEvent of desc.activationEvents) { if (activationEvent === 'onStartupFinished') { @@ -563,7 +628,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme } return Promise.all( - this._registry.getAllExtensionDescriptions().map((desc) => { + this._myRegistry.getAllExtensionDescriptions().map((desc) => { return this._handleWorkspaceContainsEagerExtension(folders, desc); }) ).then(() => { }); @@ -645,12 +710,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme } public async $extensionTestsExit(code: number): Promise<void> { - this._logService.info(`Extension host terminating: test runner requested exit with code ${code}`); - if (this._hostUtils.pid) { - this._logService.info(`Extension host with pid ${this._hostUtils.pid} exiting with code ${code}`); - } - this._logService.flush(); - this._hostUtils.exit(code); + this.terminate(`test runner requested exit with code ${code}`, code); } private _startExtensionHost(): Promise<void> { @@ -777,8 +837,29 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme return result; } - public $startExtensionHost(enabledExtensionIds: ExtensionIdentifier[]): Promise<void> { - this._registry.keepOnly(enabledExtensionIds); + private static _applyExtensionsDelta(oldGlobalRegistry: ExtensionDescriptionRegistry, oldMyRegistry: ExtensionDescriptionRegistry, extensionsDelta: IExtensionDescriptionDelta) { + const globalRegistry = new ExtensionDescriptionRegistry(oldGlobalRegistry.getAllExtensionDescriptions()); + globalRegistry.deltaExtensions(extensionsDelta.toAdd, extensionsDelta.toRemove); + + const myExtensionsSet = extensionIdentifiersArrayToSet(oldMyRegistry.getAllExtensionDescriptions().map(extension => extension.identifier)); + for (const extensionId of extensionsDelta.myToRemove) { + myExtensionsSet.delete(ExtensionIdentifier.toKey(extensionId)); + } + for (const extensionId of extensionsDelta.myToAdd) { + myExtensionsSet.add(ExtensionIdentifier.toKey(extensionId)); + } + const myExtensions = filterExtensions(globalRegistry, myExtensionsSet); + + return { globalRegistry, myExtensions }; + } + + public $startExtensionHost(extensionsDelta: IExtensionDescriptionDelta): Promise<void> { + extensionsDelta.toAdd.forEach((extension) => (<any>extension).extensionLocation = URI.revive(extension.extensionLocation)); + + const { globalRegistry, myExtensions } = AbstractExtHostExtensionService._applyExtensionsDelta(this._globalRegistry, this._myRegistry, extensionsDelta); + this._globalRegistry.set(globalRegistry.getAllExtensionDescriptions()); + this._myRegistry.set(myExtensions); + return this._startExtensionHost(); } @@ -795,7 +876,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme public async $activate(extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise<boolean> { await this._readyToRunExtensions.wait(); - if (!this._registry.getExtensionDescription(extensionId)) { + if (!this._myRegistry.getExtensionDescription(extensionId)) { // unknown extension => ignore return false; } @@ -803,24 +884,17 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme return true; } - public async $deltaExtensions(toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[]): Promise<void> { - toAdd.forEach((extension) => (<any>extension).extensionLocation = URI.revive(extension.extensionLocation)); + public async $deltaExtensions(extensionsDelta: IExtensionDescriptionDelta): Promise<void> { + extensionsDelta.toAdd.forEach((extension) => (<any>extension).extensionLocation = URI.revive(extension.extensionLocation)); - const trie = await this.getExtensionPathIndex(); + // First build up and update the trie and only afterwards apply the delta + const { globalRegistry, myExtensions } = AbstractExtHostExtensionService._applyExtensionsDelta(this._globalRegistry, this._myRegistry, extensionsDelta); + const newSearchTree = await this._createExtensionPathIndex(myExtensions); + const extensionsPaths = await this.getExtensionPathIndex(); + extensionsPaths.setSearchTree(newSearchTree); + this._globalRegistry.set(globalRegistry.getAllExtensionDescriptions()); + this._myRegistry.set(myExtensions); - await Promise.all(toRemove.map(async (extensionId) => { - const extensionDescription = this._registry.getExtensionDescription(extensionId); - if (extensionDescription) { - trie.delete(await this._realPathExtensionUri(extensionDescription.extensionLocation)); - } - })); - - await Promise.all(toAdd.map(async (extensionDescription) => { - const realpathUri = await this._realPathExtensionUri(extensionDescription.extensionLocation); - trie.set(realpathUri, extensionDescription); - })); - - this._registry.deltaExtensions(toAdd, toRemove); return Promise.resolve(undefined); } @@ -885,12 +959,13 @@ export const IExtHostExtensionService = createDecorator<IExtHostExtensionService export interface IExtHostExtensionService extends AbstractExtHostExtensionService { readonly _serviceBrand: undefined; initialize(): Promise<void>; + terminate(reason: string): void; + getExtension(extensionId: string): Promise<IExtensionDescription | undefined>; isActivated(extensionId: ExtensionIdentifier): boolean; activateByIdWithErrors(extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise<void>; - deactivateAll(): Promise<void>; getExtensionExports(extensionId: ExtensionIdentifier): IExtensionAPI | null | undefined; getExtensionRegistry(): Promise<ExtensionDescriptionRegistry>; - getExtensionPathIndex(): Promise<TernarySearchTree<URI, IExtensionDescription>>; + getExtensionPathIndex(): Promise<ExtensionPaths>; registerRemoteAuthorityResolver(authorityPrefix: string, resolver: vscode.RemoteAuthorityResolver): vscode.Disposable; onDidChangeRemoteConnectionData: Event<void>; @@ -908,8 +983,9 @@ export class Extension<T extends object | null | undefined> implements vscode.Ex readonly extensionPath: string; readonly packageJSON: IExtensionDescription; readonly extensionKind: vscode.ExtensionKind; + readonly isFromDifferentExtensionHost: boolean; - constructor(extensionService: IExtHostExtensionService, originExtensionId: ExtensionIdentifier, description: IExtensionDescription, kind: ExtensionKind) { + constructor(extensionService: IExtHostExtensionService, originExtensionId: ExtensionIdentifier, description: IExtensionDescription, kind: ExtensionKind, isFromDifferentExtensionHost: boolean) { this.#extensionService = extensionService; this.#originExtensionId = originExtensionId; this.#identifier = description.identifier; @@ -918,24 +994,36 @@ export class Extension<T extends object | null | undefined> implements vscode.Ex this.extensionPath = path.normalize(originalFSPath(description.extensionLocation)); this.packageJSON = description; this.extensionKind = kind; + this.isFromDifferentExtensionHost = isFromDifferentExtensionHost; } get isActive(): boolean { + // TODO@alexdima support this return this.#extensionService.isActivated(this.#identifier); } get exports(): T { - if (this.packageJSON.api === 'none') { + if (this.packageJSON.api === 'none' || this.isFromDifferentExtensionHost) { return undefined!; // Strict nulloverride - Public api } return <T>this.#extensionService.getExtensionExports(this.#identifier); } - activate(): Thenable<T> { - return this.#extensionService.activateByIdWithErrors(this.#identifier, { startup: false, extensionId: this.#originExtensionId, activationEvent: 'api' }).then(() => this.exports); + async activate(): Promise<T> { + if (this.isFromDifferentExtensionHost) { + throw new Error('Cannot activate foreign extension'); // TODO@alexdima support this + } + await this.#extensionService.activateByIdWithErrors(this.#identifier, { startup: false, extensionId: this.#originExtensionId, activationEvent: 'api' }); + return this.exports; } } +function filterExtensions(globalRegistry: ExtensionDescriptionRegistry, desiredExtensions: Set<string>): IExtensionDescription[] { + return globalRegistry.getAllExtensionDescriptions().filter( + extension => desiredExtensions.has(ExtensionIdentifier.toKey(extension.identifier)) + ); +} + function getRemoteAuthorityPrefix(remoteAuthority: string): string { const plusIndex = remoteAuthority.indexOf('+'); if (plusIndex === -1) { @@ -943,3 +1031,22 @@ function getRemoteAuthorityPrefix(remoteAuthority: string): string { } return remoteAuthority.substring(0, plusIndex); } + +export class ExtensionPaths { + + constructor( + private _searchTree: TernarySearchTree<URI, IExtensionDescription> + ) { } + + setSearchTree(searchTree: TernarySearchTree<URI, IExtensionDescription>): void { + this._searchTree = searchTree; + } + + findSubstr(key: URI): IExtensionDescription | undefined { + return this._searchTree.findSubstr(key); + } + + forEach(callback: (value: IExtensionDescription, index: URI) => any): void { + return this._searchTree.forEach(callback); + } +} diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 2cc57319c69..ef72e012647 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -42,13 +42,10 @@ import { Dto } from 'vs/workbench/services/extensions/common/proxyIdentifier'; class DocumentSymbolAdapter { - private _documents: ExtHostDocuments; - private _provider: vscode.DocumentSymbolProvider; - - constructor(documents: ExtHostDocuments, provider: vscode.DocumentSymbolProvider) { - this._documents = documents; - this._provider = provider; - } + constructor( + private readonly _documents: ExtHostDocuments, + private readonly _provider: vscode.DocumentSymbolProvider + ) { } async provideDocumentSymbols(resource: URI, token: CancellationToken): Promise<languages.DocumentSymbol[] | undefined> { const doc = this._documents.getDocument(resource); @@ -256,7 +253,7 @@ class HoverAdapter { private readonly _provider: vscode.HoverProvider, ) { } - public async provideHover(resource: URI, position: IPosition, token: CancellationToken): Promise<languages.Hover | undefined> { + async provideHover(resource: URI, position: IPosition, token: CancellationToken): Promise<languages.Hover | undefined> { const doc = this._documents.getDocument(resource); const pos = typeConvert.Position.to(position); @@ -464,7 +461,7 @@ class CodeActionAdapter { return { cacheId, actions }; } - public async resolveCodeAction(id: extHostProtocol.ChainedCacheId, token: CancellationToken): Promise<extHostProtocol.IWorkspaceEditDto | undefined> { + async resolveCodeAction(id: extHostProtocol.ChainedCacheId, token: CancellationToken): Promise<extHostProtocol.IWorkspaceEditDto | undefined> { const [sessionId, itemId] = id; const item = this._cache.get(sessionId, itemId); if (!item || CodeActionAdapter._isCommand(item)) { @@ -479,7 +476,7 @@ class CodeActionAdapter { : undefined; } - public releaseCodeActions(cachedId: number): void { + releaseCodeActions(cachedId: number): void { this._disposables.get(cachedId)?.dispose(); this._disposables.delete(cachedId); this._cache.delete(cachedId); @@ -697,8 +694,8 @@ class RenameAdapter { class SemanticTokensPreviousResult { constructor( - public readonly resultId: string | undefined, - public readonly tokens?: Uint32Array, + readonly resultId: string | undefined, + readonly tokens?: Uint32Array, ) { } } @@ -849,8 +846,7 @@ export class DocumentRangeSemanticTokensAdapter { constructor( private readonly _documents: ExtHostDocuments, private readonly _provider: vscode.DocumentRangeSemanticTokensProvider, - ) { - } + ) { } async provideDocumentRangeSemanticTokens(resource: URI, range: IRange, token: CancellationToken): Promise<VSBuffer | null> { const doc = this._documents.getDocument(resource); @@ -870,7 +866,7 @@ export class DocumentRangeSemanticTokensAdapter { } } -class SuggestAdapter { +class CompletionsAdapter { static supportsResolving(provider: vscode.CompletionItemProvider): boolean { return typeof provider.resolveCompletionItem === 'function'; @@ -915,7 +911,7 @@ class SuggestAdapter { const list = Array.isArray(itemsOrList) ? new CompletionList(itemsOrList) : itemsOrList; // keep result for providers that support resolving - const pid: number = SuggestAdapter.supportsResolving(this._provider) ? this._cache.add(list.items) : this._cache.add([]); + const pid: number = CompletionsAdapter.supportsResolving(this._provider) ? this._cache.add(list.items) : this._cache.add([]); const disposables = new DisposableStore(); this._disposables.set(pid, disposables); @@ -1027,9 +1023,13 @@ class SuggestAdapter { } class InlineCompletionAdapterBase { - public async provideInlineCompletions(resource: URI, position: IPosition, context: languages.InlineCompletionContext, token: CancellationToken): Promise<extHostProtocol.IdentifiableInlineCompletions | undefined> { + async provideInlineCompletions(resource: URI, position: IPosition, context: languages.InlineCompletionContext, token: CancellationToken): Promise<extHostProtocol.IdentifiableInlineCompletions | undefined> { return undefined; } + + disposeCompletions(pid: number): void { } + + handleDidShowCompletionItem(pid: number, idx: number): void { } } class InlineCompletionAdapter extends InlineCompletionAdapterBase { @@ -1044,7 +1044,7 @@ class InlineCompletionAdapter extends InlineCompletionAdapterBase { super(); } - public override async provideInlineCompletions(resource: URI, position: IPosition, context: languages.InlineCompletionContext, token: CancellationToken): Promise<extHostProtocol.IdentifiableInlineCompletions | undefined> { + override async provideInlineCompletions(resource: URI, position: IPosition, context: languages.InlineCompletionContext, token: CancellationToken): Promise<extHostProtocol.IdentifiableInlineCompletions | undefined> { const doc = this._documents.getDocument(resource); const pos = typeConvert.Position.to(position); @@ -1104,7 +1104,7 @@ class InlineCompletionAdapter extends InlineCompletionAdapterBase { }; } - public disposeCompletions(pid: number) { + override disposeCompletions(pid: number) { this._cache.delete(pid); const d = this._disposables.get(pid); if (d) { @@ -1113,7 +1113,7 @@ class InlineCompletionAdapter extends InlineCompletionAdapterBase { this._disposables.delete(pid); } - public handleDidShowCompletionItem(pid: number, idx: number): void { + override handleDidShowCompletionItem(pid: number, idx: number): void { const completionItem = this._cache.get(pid, idx); if (completionItem) { InlineCompletionController.get(this._provider).fireOnDidShowCompletionItem({ @@ -1124,8 +1124,10 @@ class InlineCompletionAdapter extends InlineCompletionAdapterBase { } class InlineCompletionAdapterNew extends InlineCompletionAdapterBase { - private readonly _cache = new Cache<vscode.InlineCompletionItemNew>('InlineCompletionItemNew'); - private readonly _disposables = new Map<number, DisposableStore>(); + private readonly _references = new ReferenceMap<{ + dispose(): void; + items: readonly vscode.InlineCompletionItemNew[]; + }>(); private readonly isAdditionProposedApiEnabled = isProposedApiEnabled(this.extension, 'inlineCompletionsAdditions'); @@ -1143,7 +1145,7 @@ class InlineCompletionAdapterNew extends InlineCompletionAdapterBase { [languages.InlineCompletionTriggerKind.Explicit]: InlineCompletionTriggerKindNew.Invoke, }; - public override async provideInlineCompletions(resource: URI, position: IPosition, context: languages.InlineCompletionContext, token: CancellationToken): Promise<extHostProtocol.IdentifiableInlineCompletions | undefined> { + override async provideInlineCompletions(resource: URI, position: IPosition, context: languages.InlineCompletionContext, token: CancellationToken): Promise<extHostProtocol.IdentifiableInlineCompletions | undefined> { const doc = this._documents.getDocument(resource); const pos = typeConvert.Position.to(position); @@ -1170,9 +1172,17 @@ class InlineCompletionAdapterNew extends InlineCompletionAdapterBase { } const normalizedResult = isArray(result) ? result : result.items; + const commands = isArray(result) ? [] : result.commands || []; - const pid = this._cache.add(normalizedResult); let disposableStore: DisposableStore | undefined = undefined; + const pid = this._references.createReferenceId({ + dispose() { + if (disposableStore) { + disposableStore.dispose(); + } + }, + items: normalizedResult + }); return { pid, @@ -1181,7 +1191,6 @@ class InlineCompletionAdapterNew extends InlineCompletionAdapterBase { if (item.command) { if (!disposableStore) { disposableStore = new DisposableStore(); - this._disposables.set(pid, disposableStore); } command = this._commands.toInternal(item.command, disposableStore); } @@ -1196,32 +1205,55 @@ class InlineCompletionAdapterNew extends InlineCompletionAdapterBase { completeBracketPairs: this.isAdditionProposedApiEnabled ? item.completeBracketPairs : false }); }), + commands: commands.map(c => { + if (!disposableStore) { + disposableStore = new DisposableStore(); + } + return this._commands.toInternal(c, disposableStore); + }) }; } - public disposeCompletions(pid: number) { - this._cache.delete(pid); - const d = this._disposables.get(pid); - if (d) { - d.clear(); - } - this._disposables.delete(pid); + override disposeCompletions(pid: number) { + const data = this._references.disposeReferenceId(pid); + data?.dispose(); } - public handleDidShowCompletionItem(pid: number, idx: number): void { - const completionItem = this._cache.get(pid, idx); + override handleDidShowCompletionItem(pid: number, idx: number): void { + const completionItem = this._references.get(pid)?.items[idx]; if (completionItem) { - if (this._provider.handleDidShowCompletionItem && isProposedApiEnabled(this.extension, 'inlineCompletionsAdditions')) { + if (this._provider.handleDidShowCompletionItem && this.isAdditionProposedApiEnabled) { this._provider.handleDidShowCompletionItem(completionItem); } } } } +class ReferenceMap<T> { + private readonly _references = new Map<number, T>(); + private _idPool = 1; + + createReferenceId(value: T): number { + const id = this._idPool++; + this._references.set(id, value); + return id; + } + + disposeReferenceId(referenceId: number): T | undefined { + const value = this._references.get(referenceId); + this._references.delete(referenceId); + return value; + } + + get(referenceId: number): T | undefined { + return this._references.get(referenceId); + } +} + export class InlineCompletionController<T extends vscode.InlineCompletionItem> implements vscode.InlineCompletionController<T> { private static readonly map = new WeakMap<vscode.InlineCompletionItemProvider<any>, InlineCompletionController<any>>(); - public static get<T extends vscode.InlineCompletionItem>(provider: vscode.InlineCompletionItemProvider<T>): InlineCompletionController<T> { + static get<T extends vscode.InlineCompletionItem>(provider: vscode.InlineCompletionItemProvider<T>): InlineCompletionController<T> { let existing = InlineCompletionController.map.get(provider); if (!existing) { existing = new InlineCompletionController(); @@ -1231,9 +1263,9 @@ export class InlineCompletionController<T extends vscode.InlineCompletionItem> i } private readonly _onDidShowCompletionItemEmitter = new Emitter<vscode.InlineCompletionItemDidShowEvent<T>>(); - public readonly onDidShowCompletionItem: vscode.Event<vscode.InlineCompletionItemDidShowEvent<T>> = this._onDidShowCompletionItemEmitter.event; + readonly onDidShowCompletionItem: vscode.Event<vscode.InlineCompletionItemDidShowEvent<T>> = this._onDidShowCompletionItemEmitter.event; - public fireOnDidShowCompletionItem(event: vscode.InlineCompletionItemDidShowEvent<T>): void { + fireOnDidShowCompletionItem(event: vscode.InlineCompletionItemDidShowEvent<T>): void { this._onDidShowCompletionItemEmitter.fire(event); } } @@ -1738,7 +1770,7 @@ class DocumentOnDropAdapter { type Adapter = DocumentSymbolAdapter | CodeLensAdapter | DefinitionAdapter | HoverAdapter | DocumentHighlightAdapter | ReferenceAdapter | CodeActionAdapter | DocumentFormattingAdapter | RangeFormattingAdapter | OnTypeFormattingAdapter | NavigateTypeAdapter | RenameAdapter - | SuggestAdapter | SignatureHelpAdapter | LinkProviderAdapter | ImplementationAdapter + | CompletionsAdapter | SignatureHelpAdapter | LinkProviderAdapter | ImplementationAdapter | TypeDefinitionAdapter | ColorProviderAdapter | FoldingProviderAdapter | DeclarationAdapter | SelectionRangeAdapter | CallHierarchyAdapter | TypeHierarchyAdapter | DocumentSemanticTokensAdapter | DocumentRangeSemanticTokensAdapter @@ -2158,21 +2190,21 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF // --- suggestion registerCompletionItemProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.CompletionItemProvider, triggerCharacters: string[]): vscode.Disposable { - const handle = this._addNewAdapter(new SuggestAdapter(this._documents, this._commands.converter, provider, this._apiDeprecation, extension), extension); - this._proxy.$registerSuggestSupport(handle, this._transformDocumentSelector(selector), triggerCharacters, SuggestAdapter.supportsResolving(provider), `${extension.identifier.value}(${triggerCharacters.join('')})`); + const handle = this._addNewAdapter(new CompletionsAdapter(this._documents, this._commands.converter, provider, this._apiDeprecation, extension), extension); + this._proxy.$registerCompletionsProvider(handle, this._transformDocumentSelector(selector), triggerCharacters, CompletionsAdapter.supportsResolving(provider), `${extension.identifier.value}(${triggerCharacters.join('')})`); return this._createDisposable(handle); } $provideCompletionItems(handle: number, resource: UriComponents, position: IPosition, context: languages.CompletionContext, token: CancellationToken): Promise<extHostProtocol.ISuggestResultDto | undefined> { - return this._withAdapter(handle, SuggestAdapter, adapter => adapter.provideCompletionItems(URI.revive(resource), position, context, token), undefined, token); + return this._withAdapter(handle, CompletionsAdapter, adapter => adapter.provideCompletionItems(URI.revive(resource), position, context, token), undefined, token); } $resolveCompletionItem(handle: number, id: extHostProtocol.ChainedCacheId, token: CancellationToken): Promise<extHostProtocol.ISuggestDataDto | undefined> { - return this._withAdapter(handle, SuggestAdapter, adapter => adapter.resolveCompletionItem(id, token), undefined, token); + return this._withAdapter(handle, CompletionsAdapter, adapter => adapter.resolveCompletionItem(id, token), undefined, token); } $releaseCompletionItems(handle: number, id: number): void { - this._withAdapter(handle, SuggestAdapter, adapter => adapter.releaseCompletionItems(id), undefined, undefined); + this._withAdapter(handle, CompletionsAdapter, adapter => adapter.releaseCompletionItems(id), undefined, undefined); } // --- ghost test @@ -2194,13 +2226,13 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF } $handleInlineCompletionDidShow(handle: number, pid: number, idx: number): void { - this._withAdapter(handle, InlineCompletionAdapter, async adapter => { + this._withAdapter(handle, InlineCompletionAdapterBase, async adapter => { adapter.handleDidShowCompletionItem(pid, idx); }, undefined, undefined); } $freeInlineCompletionsList(handle: number, pid: number): void { - this._withAdapter(handle, InlineCompletionAdapter, async adapter => { adapter.disposeCompletions(pid); }, undefined, undefined); + this._withAdapter(handle, InlineCompletionAdapterBase, async adapter => { adapter.disposeCompletions(pid); }, undefined, undefined); } // --- parameter hints diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index 6d68184d52c..7c9cc4c0eb0 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -523,12 +523,12 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { const commandDataToNotebook = new ApiCommand( 'vscode.executeDataToNotebook', '_executeDataToNotebook', 'Invoke notebook serializer', [notebookTypeArg, new ApiCommandArgument<Uint8Array, VSBuffer>('data', 'Bytes to convert to data', v => v instanceof Uint8Array, v => VSBuffer.wrap(v))], - new ApiCommandResult<NotebookDataDto, vscode.NotebookData>('Notebook Data', dto => typeConverters.NotebookData.to(dto)) + new ApiCommandResult<SerializableObjectWithBuffers<NotebookDataDto>, vscode.NotebookData>('Notebook Data', data => typeConverters.NotebookData.to(data.value)) ); const commandNotebookToData = new ApiCommand( 'vscode.executeNotebookToData', '_executeNotebookToData', 'Invoke notebook serializer', - [notebookTypeArg, new ApiCommandArgument<vscode.NotebookData, NotebookDataDto>('NotebookData', 'Notebook data to convert to bytes', v => true, v => typeConverters.NotebookData.from(v))], + [notebookTypeArg, new ApiCommandArgument<vscode.NotebookData, SerializableObjectWithBuffers<NotebookDataDto>>('NotebookData', 'Notebook data to convert to bytes', v => true, v => new SerializableObjectWithBuffers(typeConverters.NotebookData.from(v)))], new ApiCommandResult<VSBuffer, Uint8Array>('Bytes', dto => dto.buffer) ); diff --git a/src/vs/workbench/api/common/extHostNotebookConcatDocument.ts b/src/vs/workbench/api/common/extHostNotebookConcatDocument.ts deleted file mode 100644 index 65ecebc97c1..00000000000 --- a/src/vs/workbench/api/common/extHostNotebookConcatDocument.ts +++ /dev/null @@ -1,192 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as types from 'vs/workbench/api/common/extHostTypes'; -import * as vscode from 'vscode'; -import { Event, Emitter } from 'vs/base/common/event'; -import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; -import { PrefixSumComputer } from 'vs/editor/common/model/prefixSumComputer'; -import { DisposableStore } from 'vs/base/common/lifecycle'; -import { score } from 'vs/editor/common/languageSelector'; -import { ResourceMap } from 'vs/base/common/map'; -import { URI } from 'vs/base/common/uri'; -import { generateUuid } from 'vs/base/common/uuid'; -import { ExtHostNotebookDocuments } from 'vs/workbench/api/common/extHostNotebookDocuments'; - -export class ExtHostNotebookConcatDocument implements vscode.NotebookConcatTextDocument { - - private _disposables = new DisposableStore(); - private _isClosed = false; - - private _cells!: vscode.NotebookCell[]; - private _cellUris!: ResourceMap<number>; - private _cellLengths!: PrefixSumComputer; - private _cellLines!: PrefixSumComputer; - private _versionId = 0; - - private readonly _onDidChange = new Emitter<void>(); - readonly onDidChange: Event<void> = this._onDidChange.event; - - readonly uri = URI.from({ scheme: 'vscode-concat-doc', path: generateUuid() }); - - constructor( - extHostNotebooks: ExtHostNotebookDocuments, - extHostDocuments: ExtHostDocuments, - private readonly _notebook: vscode.NotebookDocument, - private readonly _selector: vscode.DocumentSelector | undefined, - ) { - this._init(); - - this._disposables.add(extHostDocuments.onDidChangeDocument(e => { - const cellIdx = this._cellUris.get(e.document.uri); - if (cellIdx !== undefined) { - this._cellLengths.setValue(cellIdx, this._cells[cellIdx].document.getText().length + 1); - this._cellLines.setValue(cellIdx, this._cells[cellIdx].document.lineCount); - this._versionId += 1; - this._onDidChange.fire(undefined); - } - })); - const documentChange = (document: vscode.NotebookDocument) => { - if (document === this._notebook) { - this._init(); - this._versionId += 1; - this._onDidChange.fire(undefined); - } - }; - - this._disposables.add(extHostNotebooks.onDidChangeNotebookDocument(e => documentChange(e.notebook))); - } - - dispose(): void { - this._disposables.dispose(); - this._isClosed = true; - } - - get isClosed() { - return this._isClosed; - } - - private _init() { - this._cells = []; - this._cellUris = new ResourceMap(); - const cellLengths: number[] = []; - const cellLineCounts: number[] = []; - for (const cell of this._notebook.getCells()) { - if (cell.kind === types.NotebookCellKind.Code && (!this._selector || score(this._selector, cell.document.uri, cell.document.languageId, true, undefined))) { - this._cellUris.set(cell.document.uri, this._cells.length); - this._cells.push(cell); - cellLengths.push(cell.document.getText().length + 1); - cellLineCounts.push(cell.document.lineCount); - } - } - this._cellLengths = new PrefixSumComputer(new Uint32Array(cellLengths)); - this._cellLines = new PrefixSumComputer(new Uint32Array(cellLineCounts)); - } - - get version(): number { - return this._versionId; - } - - getText(range?: vscode.Range): string { - if (!range) { - let result = ''; - for (const cell of this._cells) { - result += cell.document.getText() + '\n'; - } - // remove last newline again - result = result.slice(0, -1); - return result; - } - - if (range.isEmpty) { - return ''; - } - - // get start and end locations and create substrings - const start = this.locationAt(range.start); - const end = this.locationAt(range.end); - - const startIdx = this._cellUris.get(start.uri); - const endIdx = this._cellUris.get(end.uri); - - if (startIdx === undefined || endIdx === undefined) { - return ''; - } - - if (startIdx === endIdx) { - return this._cells[startIdx].document.getText(new types.Range(start.range.start, end.range.end)); - } - - const parts = [this._cells[startIdx].document.getText(new types.Range(start.range.start, new types.Position(this._cells[startIdx].document.lineCount, 0)))]; - for (let i = startIdx + 1; i < endIdx; i++) { - parts.push(this._cells[i].document.getText()); - } - parts.push(this._cells[endIdx].document.getText(new types.Range(new types.Position(0, 0), end.range.end))); - return parts.join('\n'); - } - - offsetAt(position: vscode.Position): number { - const idx = this._cellLines.getIndexOf(position.line); - const offset1 = this._cellLengths.getPrefixSum(idx.index - 1); - const offset2 = this._cells[idx.index].document.offsetAt(position.with(idx.remainder)); - return offset1 + offset2; - } - - positionAt(locationOrOffset: vscode.Location | number): vscode.Position { - if (typeof locationOrOffset === 'number') { - const idx = this._cellLengths.getIndexOf(locationOrOffset); - const lineCount = this._cellLines.getPrefixSum(idx.index - 1); - return this._cells[idx.index].document.positionAt(idx.remainder).translate(lineCount); - } - - const idx = this._cellUris.get(locationOrOffset.uri); - if (idx !== undefined) { - const line = this._cellLines.getPrefixSum(idx - 1); - return new types.Position(line + locationOrOffset.range.start.line, locationOrOffset.range.start.character); - } - // do better? - // return undefined; - return new types.Position(0, 0); - } - - locationAt(positionOrRange: vscode.Range | vscode.Position): types.Location { - if (!types.Range.isRange(positionOrRange)) { - positionOrRange = new types.Range(<types.Position>positionOrRange, <types.Position>positionOrRange); - } - - const startIdx = this._cellLines.getIndexOf(positionOrRange.start.line); - let endIdx = startIdx; - if (!positionOrRange.isEmpty) { - endIdx = this._cellLines.getIndexOf(positionOrRange.end.line); - } - - const startPos = new types.Position(startIdx.remainder, positionOrRange.start.character); - const endPos = new types.Position(endIdx.remainder, positionOrRange.end.character); - const range = new types.Range(startPos, endPos); - - const startCell = this._cells[startIdx.index]; - return new types.Location(startCell.document.uri, <types.Range>startCell.document.validateRange(range)); - } - - contains(uri: vscode.Uri): boolean { - return this._cellUris.has(uri); - } - - validateRange(range: vscode.Range): vscode.Range { - const start = this.validatePosition(range.start); - const end = this.validatePosition(range.end); - return range.with(start, end); - } - - validatePosition(position: vscode.Position): vscode.Position { - const startIdx = this._cellLines.getIndexOf(position.line); - - const cellPosition = new types.Position(startIdx.remainder, position.character); - const validCellPosition = this._cells[startIdx.index].document.validatePosition(cellPosition); - - const line = this._cellLines.getPrefixSum(startIdx.index - 1); - return new types.Position(line + validCellPosition.line, validCellPosition.character); - } -} diff --git a/src/vs/workbench/api/common/extHostNotebookDocument.ts b/src/vs/workbench/api/common/extHostNotebookDocument.ts index e8cc8e027ff..b77133347e3 100644 --- a/src/vs/workbench/api/common/extHostNotebookDocument.ts +++ b/src/vs/workbench/api/common/extHostNotebookDocument.ts @@ -218,11 +218,11 @@ export class ExtHostNotebookDocument { const result = { notebook: this.apiNotebook, metadata: newMetadata, - cellChanges: <vscode.NotebookDocumentContentCellChange[]>[], + cellChanges: <vscode.NotebookDocumentCellChange[]>[], contentChanges: <vscode.NotebookDocumentContentChange[]>[], }; - type RelaxedCellChange = Partial<vscode.NotebookDocumentContentCellChange> & { cell: vscode.NotebookCell }; + type RelaxedCellChange = Partial<vscode.NotebookDocumentCellChange> & { cell: vscode.NotebookCell }; const relaxedCellChanges: RelaxedCellChange[] = []; // -- apply change and populate content changes diff --git a/src/vs/workbench/api/common/extHostNotebookKernels.ts b/src/vs/workbench/api/common/extHostNotebookKernels.ts index 05550b53475..f99ead59ac7 100644 --- a/src/vs/workbench/api/common/extHostNotebookKernels.ts +++ b/src/vs/workbench/api/common/extHostNotebookKernels.ts @@ -108,7 +108,7 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { const onDidReceiveMessage = new Emitter<{ editor: vscode.NotebookEditor; message: any }>(); const data: INotebookKernelDto2 = { - id: createKernelId(extension, id), + id: createKernelId(extension.identifier, id), notebookType: viewType, extensionId: extension.identifier, extensionLocation: extension.extensionLocation, @@ -218,7 +218,7 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { that._logService.trace(`NotebookController[${handle}] NOT associated to notebook, associated to THESE notebooks:`, Array.from(associatedNotebooks.keys()).map(u => u.toString())); throw new Error(`notebook controller is NOT associated to notebook: ${cell.notebook.uri.toString()}`); } - return that._createNotebookCellExecution(cell, createKernelId(extension, this.id)); + return that._createNotebookCellExecution(cell, createKernelId(extension.identifier, this.id)); }, dispose: () => { if (!isDisposed) { @@ -257,6 +257,15 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { return controller; } + getIdByController(controller: vscode.NotebookController) { + for (const [_, candidate] of this._kernelData) { + if (candidate.controller === controller) { + return createKernelId(candidate.extensionId, controller.id); + } + } + return null; + } + $acceptNotebookAssociation(handle: number, uri: UriComponents, value: boolean): void { const obj = this._kernelData.get(handle); if (obj) { @@ -583,6 +592,6 @@ class TimeoutBasedCollector<T> { } } -function createKernelId(extension: IExtensionDescription, id: string): string { - return `${extension.identifier.value}/${id}`; +export function createKernelId(extensionIdentifier: ExtensionIdentifier, id: string): string { + return `${extensionIdentifier.value}/${id}`; } diff --git a/src/vs/workbench/api/common/extHostNotebookProxyKernels.ts b/src/vs/workbench/api/common/extHostNotebookProxyKernels.ts new file mode 100644 index 00000000000..786101bf360 --- /dev/null +++ b/src/vs/workbench/api/common/extHostNotebookProxyKernels.ts @@ -0,0 +1,157 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter } from 'vs/base/common/event'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ResourceMap } from 'vs/base/common/map'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ILogService } from 'vs/platform/log/common/log'; +import { ExtHostNotebookProxyKernelsShape, IMainContext, INotebookProxyKernelDto, MainContext, MainThreadNotebookProxyKernelsShape } from 'vs/workbench/api/common/extHost.protocol'; +import { createKernelId, ExtHostNotebookKernels } from 'vs/workbench/api/common/extHostNotebookKernels'; +import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; +import * as vscode from 'vscode'; + +interface IProxyKernelData { + extensionId: ExtensionIdentifier; + controller: vscode.NotebookProxyController; + onDidChangeSelection: Emitter<{ selected: boolean; notebook: vscode.NotebookDocument }>; + associatedNotebooks: ResourceMap<boolean>; +} + +export type SelectKernelReturnArgs = ControllerInfo | { notebookEditorId: string } | ControllerInfo & { notebookEditorId: string } | undefined; +type ControllerInfo = { id: string; extension: string }; + + +export class ExtHostNotebookProxyKernels implements ExtHostNotebookProxyKernelsShape { + + private readonly _proxy: MainThreadNotebookProxyKernelsShape; + + private readonly _proxyKernelData: Map<number, IProxyKernelData> = new Map<number, IProxyKernelData>(); + private _handlePool: number = 0; + + private readonly _onDidChangeCellExecutionState = new Emitter<vscode.NotebookCellExecutionStateChangeEvent>(); + readonly onDidChangeNotebookCellExecutionState = this._onDidChangeCellExecutionState.event; + + constructor( + mainContext: IMainContext, + private readonly extHostNotebook: ExtHostNotebookKernels, + @ILogService private readonly _logService: ILogService + ) { + this._proxy = mainContext.getProxy(MainContext.MainThreadNotebookProxyKernels); + } + + createNotebookProxyController(extension: IExtensionDescription, id: string, viewType: string, label: string, handler: () => vscode.NotebookController | string | Thenable<vscode.NotebookController | string>): vscode.NotebookProxyController { + const handle = this._handlePool++; + + let isDisposed = false; + const commandDisposables = new DisposableStore(); + const onDidChangeSelection = new Emitter<{ selected: boolean; notebook: vscode.NotebookDocument }>(); + + const data: INotebookProxyKernelDto = { + id: createKernelId(extension.identifier, id), + notebookType: viewType, + extensionId: extension.identifier, + extensionLocation: extension.extensionLocation, + label: label || extension.identifier.value, + }; + + let _resolveHandler = handler; + + this._proxy.$addProxyKernel(handle, data).catch(err => { + // this can happen when a kernel with that ID is already registered + console.log(err); + isDisposed = true; + }); + + let tokenPool = 0; + const _update = () => { + if (isDisposed) { + return; + } + const myToken = ++tokenPool; + Promise.resolve().then(() => { + if (myToken === tokenPool) { + this._proxy.$updateProxyKernel(handle, data); + } + }); + }; + + // notebook documents that are associated to this controller + const associatedNotebooks = new ResourceMap<boolean>(); + + const controller: vscode.NotebookProxyController = { + get id() { return id; }, + get notebookType() { return data.notebookType; }, + onDidChangeSelectedNotebooks: onDidChangeSelection.event, + get label() { + return data.label; + }, + set label(value) { + data.label = value ?? extension.displayName ?? extension.name; + _update(); + }, + get detail() { + return data.detail ?? ''; + }, + set detail(value) { + data.detail = value; + _update(); + }, + get description() { + return data.description ?? ''; + }, + set description(value) { + data.description = value; + _update(); + }, + get kind() { + checkProposedApiEnabled(extension, 'notebookControllerKind'); + return data.kind ?? ''; + }, + set kind(value) { + checkProposedApiEnabled(extension, 'notebookControllerKind'); + data.kind = value; + _update(); + }, + get resolveHandler() { + return _resolveHandler; + }, + dispose: () => { + if (!isDisposed) { + this._logService.trace(`NotebookProxyController[${handle}], DISPOSED`); + isDisposed = true; + this._proxyKernelData.delete(handle); + commandDisposables.dispose(); + onDidChangeSelection.dispose(); + this._proxy.$removeProxyKernel(handle); + } + } + }; + + this._proxyKernelData.set(handle, { + extensionId: extension.identifier, + controller, + onDidChangeSelection, + associatedNotebooks + }); + return controller; + } + + async $resolveKernel(handle: number): Promise<string | null> { + const obj = this._proxyKernelData.get(handle); + if (!obj) { + // extension can dispose kernels in the meantime + return null; + } + + const controller = await obj.controller.resolveHandler(); + if (typeof controller === 'string') { + return controller; + } else { + return this.extHostNotebook.getIdByController(controller); + } + } +} + diff --git a/src/vs/workbench/api/common/extHostOutput.ts b/src/vs/workbench/api/common/extHostOutput.ts index 12e6570161e..5c013fa2534 100644 --- a/src/vs/workbench/api/common/extHostOutput.ts +++ b/src/vs/workbench/api/common/extHostOutput.ts @@ -11,7 +11,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ILogger, ILoggerService } from 'vs/platform/log/common/log'; -import { OutputChannelUpdateMode } from 'vs/workbench/contrib/output/common/output'; +import { OutputChannelUpdateMode } from 'vs/workbench/services/output/common/output'; import { IExtHostConsumerFileSystem } from 'vs/workbench/api/common/extHostFileSystemConsumer'; import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; import { IExtHostFileSystemInfo } from 'vs/workbench/api/common/extHostFileSystemInfo'; diff --git a/src/vs/workbench/api/common/extHostQuickOpen.ts b/src/vs/workbench/api/common/extHostQuickOpen.ts index 0df03b79137..a3c48d6cc54 100644 --- a/src/vs/workbench/api/common/extHostQuickOpen.ts +++ b/src/vs/workbench/api/common/extHostQuickOpen.ts @@ -3,16 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { asPromise } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter } from 'vs/base/common/event'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import { IExtHostWorkspaceProvider } from 'vs/workbench/api/common/extHostWorkspace'; -import type { InputBox, InputBoxOptions, InputBoxValidationSeverity, QuickInput, QuickInputButton, QuickPick, QuickPickItem, QuickPickItemButtonEvent, QuickPickOptions, WorkspaceFolder, WorkspaceFolderPickOptions } from 'vscode'; +import { InputBox, InputBoxOptions, InputBoxValidationMessage, QuickInput, QuickInputButton, QuickPick, QuickPickItem, QuickPickItemButtonEvent, QuickPickOptions, WorkspaceFolder, WorkspaceFolderPickOptions } from 'vscode'; import { ExtHostQuickOpenShape, IMainContext, MainContext, TransferQuickInput, TransferQuickInputButton, TransferQuickPickItemOrSeparator } from './extHost.protocol'; import { URI } from 'vs/base/common/uri'; -import { ThemeIcon, QuickInputButtons, QuickPickItemKind } from 'vs/workbench/api/common/extHostTypes'; +import { ThemeIcon, QuickInputButtons, QuickPickItemKind, InputBoxValidationSeverity } from 'vs/workbench/api/common/extHostTypes'; import { isCancellationError } from 'vs/base/common/errors'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { coalesce } from 'vs/base/common/arrays'; @@ -46,7 +45,7 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx private _commands: ExtHostCommands; private _onDidSelectItem?: (handle: number) => void; - private _validateInput?: (input: string) => string | { content: string; severity: Severity } | undefined | null | Thenable<string | { content: string; severity: Severity } | undefined | null>; + private _validateInput?: (input: string) => string | InputBoxValidationMessage | undefined | null | Thenable<string | InputBoxValidationMessage | undefined | null>; private _sessions = new Map<number, ExtHostQuickInput>(); @@ -148,7 +147,7 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx showInput(options?: InputBoxOptions, token: CancellationToken = CancellationToken.None): Promise<string | undefined> { // global validate fn used in callback below - this._validateInput = options ? options.validateInput : undefined; + this._validateInput = options ? options.validateInput2 ?? options.validateInput : undefined; return proxy.$input(options, typeof this._validateInput === 'function', token) .then(undefined, err => { @@ -160,11 +159,36 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx }); } - $validateInput(input: string): Promise<string | { content: string; severity: Severity } | null | undefined> { - if (this._validateInput) { - return asPromise(() => this._validateInput!(input)); + async $validateInput(input: string): Promise<string | { content: string; severity: Severity } | null | undefined> { + if (!this._validateInput) { + return; + } + + const result = await this._validateInput(input); + if (!result || typeof result === 'string') { + return result; } - return Promise.resolve(undefined); + + let severity: Severity; + switch (result.severity) { + case InputBoxValidationSeverity.Info: + severity = Severity.Info; + break; + case InputBoxValidationSeverity.Warning: + severity = Severity.Warning; + break; + case InputBoxValidationSeverity.Error: + severity = Severity.Error; + break; + default: + severity = result.message ? Severity.Error : Severity.Ignore; + break; + } + + return { + content: result.message, + severity + }; } // ---- workspace folder picker @@ -675,7 +699,7 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx private _password = false; private _prompt: string | undefined; private _validationMessage: string | undefined; - private _validationMessage2: string | { content: string; severity: InputBoxValidationSeverity } | undefined; + private _validationMessage2: string | InputBoxValidationMessage | undefined; constructor(private readonly extension: IExtensionDescription, onDispose: () => void) { super(extension.identifier, onDispose); @@ -713,7 +737,7 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx return this._validationMessage2; } - set validationMessage2(validationMessage: string | { content: string; severity: InputBoxValidationSeverity } | undefined) { + set validationMessage2(validationMessage: string | InputBoxValidationMessage | undefined) { checkProposedApiEnabled(this.extension, 'inputBoxSeverity'); this._validationMessage2 = validationMessage; if (!validationMessage) { @@ -721,7 +745,7 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx } else if (typeof validationMessage === 'string') { this.update({ validationMessage, severity: Severity.Error }); } else { - this.update({ validationMessage: validationMessage.content, severity: validationMessage.severity ?? Severity.Error }); + this.update({ validationMessage: validationMessage.message, severity: validationMessage.severity ?? Severity.Error }); } } } diff --git a/src/vs/workbench/api/common/extHostRequireInterceptor.ts b/src/vs/workbench/api/common/extHostRequireInterceptor.ts index 9ce9a8f402f..4a072952392 100644 --- a/src/vs/workbench/api/common/extHostRequireInterceptor.ts +++ b/src/vs/workbench/api/common/extHostRequireInterceptor.ts @@ -4,19 +4,17 @@ *--------------------------------------------------------------------------------------------*/ import * as performance from 'vs/base/common/performance'; -import { TernarySearchTree } from 'vs/base/common/map'; import { URI } from 'vs/base/common/uri'; import { MainThreadTelemetryShape, MainContext } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostConfigProvider, IExtHostConfiguration } from 'vs/workbench/api/common/extHostConfiguration'; import { nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; -import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; import * as vscode from 'vscode'; -import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { IExtensionApiFactory } from 'vs/workbench/api/common/extHost.api.impl'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { IExtensionApiFactory, IExtensionRegistries } from 'vs/workbench/api/common/extHost.api.impl'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; +import { ExtensionPaths, IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; import { platform } from 'vs/base/common/process'; import { ILogService } from 'vs/platform/log/common/log'; import { escapeRegExpCharacters } from 'vs/base/common/strings'; @@ -42,7 +40,7 @@ export abstract class RequireInterceptor { constructor( private _apiFactory: IExtensionApiFactory, - private _extensionRegistry: ExtensionDescriptionRegistry, + private _extensionRegistry: IExtensionRegistries, @IInstantiationService private readonly _instaService: IInstantiationService, @IExtHostConfiguration private readonly _extHostConfiguration: IExtHostConfiguration, @IExtHostExtensionService private readonly _extHostExtensionService: IExtHostExtensionService, @@ -156,8 +154,8 @@ class VSCodeNodeModuleFactory implements INodeModuleFactory { constructor( private readonly _apiFactory: IExtensionApiFactory, - private readonly _extensionPaths: TernarySearchTree<URI, IExtensionDescription>, - private readonly _extensionRegistry: ExtensionDescriptionRegistry, + private readonly _extensionPaths: ExtensionPaths, + private readonly _extensionRegistry: IExtensionRegistries, private readonly _configProvider: ExtHostConfigProvider, private readonly _logService: ILogService, ) { @@ -208,7 +206,7 @@ class KeytarNodeModuleFactory implements INodeModuleFactory { private _impl: IKeytarModule; constructor( - private readonly _extensionPaths: TernarySearchTree<URI, IExtensionDescription>, + private readonly _extensionPaths: ExtensionPaths, @IExtHostRpcService rpcService: IExtHostRpcService, @IExtHostInitDataService initData: IExtHostInitDataService, @@ -303,7 +301,7 @@ class OpenNodeModuleFactory implements INodeModuleFactory { private _mainThreadTelemetry: MainThreadTelemetryShape; constructor( - private readonly _extensionPaths: TernarySearchTree<URI, IExtensionDescription>, + private readonly _extensionPaths: ExtensionPaths, private readonly _appUriScheme: string, @IExtHostRpcService rpcService: IExtHostRpcService, ) { diff --git a/src/vs/workbench/api/common/extHostRpcService.ts b/src/vs/workbench/api/common/extHostRpcService.ts index 322fa65b374..c6df14903e4 100644 --- a/src/vs/workbench/api/common/extHostRpcService.ts +++ b/src/vs/workbench/api/common/extHostRpcService.ts @@ -17,12 +17,14 @@ export class ExtHostRpcService implements IExtHostRpcService { readonly getProxy: <T>(identifier: ProxyIdentifier<T>) => Proxied<T>; readonly set: <T, R extends T> (identifier: ProxyIdentifier<T>, instance: R) => R; + readonly dispose: () => void; readonly assertRegistered: (identifiers: ProxyIdentifier<any>[]) => void; readonly drain: () => Promise<void>; constructor(rpcProtocol: IRPCProtocol) { this.getProxy = rpcProtocol.getProxy.bind(rpcProtocol); this.set = rpcProtocol.set.bind(rpcProtocol); + this.dispose = rpcProtocol.dispose.bind(rpcProtocol); this.assertRegistered = rpcProtocol.assertRegistered.bind(rpcProtocol); this.drain = rpcProtocol.drain.bind(rpcProtocol); } diff --git a/src/vs/workbench/api/common/extHostTask.ts b/src/vs/workbench/api/common/extHostTask.ts index 37758d0d220..00d03d0b6ee 100644 --- a/src/vs/workbench/api/common/extHostTask.ts +++ b/src/vs/workbench/api/common/extHostTask.ts @@ -320,7 +320,7 @@ export namespace TaskDTO { result.group = types.TaskGroup.from(value.group._id); if (result.group && value.group.isDefault) { result.group = new types.TaskGroup(result.group.id, result.group.label); - if (value.group.isDefault) { + if (value.group.isDefault === true) { result.group.isDefault = value.group.isDefault; } } diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index 12643cef941..1aa1e678e00 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -38,7 +38,7 @@ export interface IExtHostTerminalService extends ExtHostTerminalServiceShape, ID onDidChangeTerminalState: Event<vscode.Terminal>; onDidWriteTerminalData: Event<vscode.TerminalDataWriteEvent>; - createTerminal(name?: string, shellPath?: string, shellArgs?: string[] | string): vscode.Terminal; + createTerminal(name?: string, shellPath?: string, shellArgs?: readonly string[] | string): vscode.Terminal; createTerminalFromOptions(options: vscode.TerminalOptions, internalOptions?: ITerminalInternalOptions): vscode.Terminal; createExtensionTerminal(options: vscode.ExtensionTerminalOptions): vscode.Terminal; attachPtyToTerminal(id: number, pty: vscode.Pseudoterminal): void; diff --git a/src/vs/workbench/api/common/extHostTimeline.ts b/src/vs/workbench/api/common/extHostTimeline.ts index baab688ad31..c9d9e611649 100644 --- a/src/vs/workbench/api/common/extHostTimeline.ts +++ b/src/vs/workbench/api/common/extHostTimeline.ts @@ -7,7 +7,7 @@ import * as vscode from 'vscode'; import { UriComponents, URI } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ExtHostTimelineShape, MainThreadTimelineShape, IMainContext, MainContext } from 'vs/workbench/api/common/extHost.protocol'; -import { Timeline, TimelineItem, TimelineOptions, TimelineProvider, InternalTimelineOptions } from 'vs/workbench/contrib/timeline/common/timeline'; +import { Timeline, TimelineItem, TimelineOptions, TimelineProvider } from 'vs/workbench/contrib/timeline/common/timeline'; import { IDisposable, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { CancellationToken } from 'vs/base/common/cancellation'; import { CommandsConverter, ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; @@ -19,7 +19,7 @@ import { isString } from 'vs/base/common/types'; export interface IExtHostTimeline extends ExtHostTimelineShape { readonly _serviceBrand: undefined; - $getTimeline(id: string, uri: UriComponents, options: vscode.TimelineOptions, token: vscode.CancellationToken, internalOptions?: InternalTimelineOptions): Promise<Timeline | undefined>; + $getTimeline(id: string, uri: UriComponents, options: vscode.TimelineOptions, token: vscode.CancellationToken): Promise<Timeline | undefined>; } export const IExtHostTimeline = createDecorator<IExtHostTimeline>('IExtHostTimeline'); @@ -51,9 +51,9 @@ export class ExtHostTimeline implements IExtHostTimeline { }); } - async $getTimeline(id: string, uri: UriComponents, options: vscode.TimelineOptions, token: vscode.CancellationToken, internalOptions?: InternalTimelineOptions): Promise<Timeline | undefined> { + async $getTimeline(id: string, uri: UriComponents, options: vscode.TimelineOptions, token: vscode.CancellationToken): Promise<Timeline | undefined> { const provider = this._providers.get(id); - return provider?.provideTimeline(URI.revive(uri), options, token, internalOptions); + return provider?.provideTimeline(URI.revive(uri), options, token); } registerTimelineProvider(scheme: string | string[], provider: vscode.TimelineProvider, _extensionId: ExtensionIdentifier, commandConverter: CommandsConverter): IDisposable { @@ -71,8 +71,8 @@ export class ExtHostTimeline implements IExtHostTimeline { ...provider, scheme: scheme, onDidChange: undefined, - async provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken, internalOptions?: InternalTimelineOptions) { - if (internalOptions?.resetCache) { + async provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken) { + if (options?.resetCache) { timelineDisposables.clear(); // For now, only allow the caching of a single Uri @@ -87,7 +87,7 @@ export class ExtHostTimeline implements IExtHostTimeline { // TODO: Should we bother converting all the data if we aren't caching? Meaning it is being requested by an extension? - const convertItem = convertTimelineItem(uri, internalOptions); + const convertItem = convertTimelineItem(uri, options); return { ...result, source: provider.id, @@ -106,7 +106,7 @@ export class ExtHostTimeline implements IExtHostTimeline { } private convertTimelineItem(source: string, commandConverter: CommandsConverter, disposables: DisposableStore) { - return (uri: URI, options?: InternalTimelineOptions) => { + return (uri: URI, options?: TimelineOptions) => { let items: Map<string, vscode.TimelineItem> | undefined; if (options?.cacheResults) { let itemsByUri = this._itemsBySourceAndUriMap.get(source); diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 9dec8f6b163..979eebec86c 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -7,6 +7,7 @@ import { asArray, coalesce, isNonEmptyArray } from 'vs/base/common/arrays'; import { VSBuffer } from 'vs/base/common/buffer'; import * as htmlContent from 'vs/base/common/htmlContent'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ResourceSet } from 'vs/base/common/map'; import { marked } from 'vs/base/common/marked/marked'; import { parse } from 'vs/base/common/marshalling'; import { cloneAndChange } from 'vs/base/common/objects'; @@ -580,7 +581,17 @@ export namespace WorkspaceEdit { }; if (value instanceof types.WorkspaceEdit) { - for (let entry of value._allEntries()) { + + // collect all files that are to be created so that their version + // information (in case they exist as text model already) can be ignored + const toCreate = new ResourceSet(); + for (const entry of value._allEntries()) { + if (entry._type === types.FileEditType.File && URI.isUri(entry.to) && entry.from === undefined) { + toCreate.add(entry.to); + } + } + + for (const entry of value._allEntries()) { if (entry._type === types.FileEditType.File) { // file operation @@ -598,7 +609,7 @@ export namespace WorkspaceEdit { _type: extHostProtocol.WorkspaceEditType.Text, resource: entry.uri, edit: TextEdit.from(entry.edit), - modelVersionId: versionInfo?.getTextDocumentVersion(entry.uri), + modelVersionId: !toCreate.has(entry.uri) ? versionInfo?.getTextDocumentVersion(entry.uri) : undefined, metadata: entry.metadata }); } else if (entry._type === types.FileEditType.Cell) { @@ -1480,7 +1491,8 @@ export namespace LanguageSelector { language: filter.language, scheme: filter.scheme, pattern: GlobPattern.from(filter.pattern), - exclusive: filter.exclusive + exclusive: filter.exclusive, + notebookType: filter.notebookType }; } } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index ca19cdc010c..a69e00f05ec 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -1594,7 +1594,10 @@ export class CompletionList { @es5ClassCompat export class InlineSuggestion implements vscode.InlineCompletionItem { - insertText?: string; + + insertText?: string | SnippetString; + + filterText?: string; /** * @deprecated Use `insertText` instead. Will be removed eventually. @@ -1604,7 +1607,7 @@ export class InlineSuggestion implements vscode.InlineCompletionItem { range?: Range; command?: vscode.Command; - constructor(insertText: string, range?: Range, command?: vscode.Command) { + constructor(insertText: string | SnippetString, range?: Range, command?: vscode.Command) { this.insertText = insertText; this.range = range; this.command = command; @@ -1640,8 +1643,11 @@ export class InlineSuggestionNew implements vscode.InlineCompletionItemNew { export class InlineSuggestionsNew implements vscode.InlineCompletionListNew { items: vscode.InlineCompletionItemNew[]; - constructor(items: vscode.InlineCompletionItemNew[]) { + commands: vscode.Command[] | undefined; + + constructor(items: vscode.InlineCompletionItemNew[], commands?: vscode.Command[]) { this.items = items; + this.commands = commands; } } diff --git a/src/vs/workbench/api/common/extHostVariableResolverService.ts b/src/vs/workbench/api/common/extHostVariableResolverService.ts new file mode 100644 index 00000000000..7455de15e22 --- /dev/null +++ b/src/vs/workbench/api/common/extHostVariableResolverService.ts @@ -0,0 +1,167 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Lazy } from 'vs/base/common/lazy'; +import { Disposable } from 'vs/base/common/lifecycle'; +import * as path from 'vs/base/common/path'; +import * as process from 'vs/base/common/process'; +import { URI } from 'vs/base/common/uri'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; +import { IExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs'; +import { IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; +import { CustomEditorTabInput, NotebookDiffEditorTabInput, NotebookEditorTabInput, TextDiffTabInput, TextTabInput } from 'vs/workbench/api/common/extHostTypes'; +import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; +import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; +import { AbstractVariableResolverService } from 'vs/workbench/services/configurationResolver/common/variableResolver'; +import * as vscode from 'vscode'; +import { ExtHostConfigProvider, IExtHostConfiguration } from './extHostConfiguration'; + +export interface IExtHostVariableResolverProvider { + readonly _serviceBrand: undefined; + getResolver(): Promise<IConfigurationResolverService>; +} + +export const IExtHostVariableResolverProvider = createDecorator<IExtHostVariableResolverProvider>('IExtHostVariableResolverProvider'); + +interface DynamicContext { + folders: vscode.WorkspaceFolder[]; +} + +class ExtHostVariableResolverService extends AbstractVariableResolverService { + + constructor( + extensionService: IExtHostExtensionService, + workspaceService: IExtHostWorkspace, + editorService: IExtHostDocumentsAndEditors, + editorTabs: IExtHostEditorTabs, + configProvider: ExtHostConfigProvider, + context: DynamicContext, + homeDir: string | undefined, + ) { + function getActiveUri(): URI | undefined { + if (editorService) { + const activeEditor = editorService.activeEditor(); + if (activeEditor) { + return activeEditor.document.uri; + } + const activeTab = editorTabs.tabGroups.all.find(group => group.isActive)?.activeTab; + if (activeTab !== undefined) { + // Resolve a resource from the tab + if (activeTab.input instanceof TextDiffTabInput || activeTab.input instanceof NotebookDiffEditorTabInput) { + return activeTab.input.modified; + } else if (activeTab.input instanceof TextTabInput || activeTab.input instanceof NotebookEditorTabInput || activeTab.input instanceof CustomEditorTabInput) { + return activeTab.input.uri; + } + } + } + return undefined; + } + + super({ + getFolderUri: (folderName: string): URI | undefined => { + const found = context.folders.filter(f => f.name === folderName); + if (found && found.length > 0) { + return found[0].uri; + } + return undefined; + }, + getWorkspaceFolderCount: (): number => { + return context.folders.length; + }, + getConfigurationValue: (folderUri: URI | undefined, section: string): string | undefined => { + return configProvider.getConfiguration(undefined, folderUri).get<string>(section); + }, + getAppRoot: (): string | undefined => { + return process.cwd(); + }, + getExecPath: (): string | undefined => { + return process.env['VSCODE_EXEC_PATH']; + }, + getFilePath: (): string | undefined => { + const activeUri = getActiveUri(); + if (activeUri) { + return path.normalize(activeUri.fsPath); + } + return undefined; + }, + getWorkspaceFolderPathForFile: (): string | undefined => { + if (workspaceService) { + const activeUri = getActiveUri(); + if (activeUri) { + const ws = workspaceService.getWorkspaceFolder(activeUri); + if (ws) { + return path.normalize(ws.uri.fsPath); + } + } + } + return undefined; + }, + getSelectedText: (): string | undefined => { + if (editorService) { + const activeEditor = editorService.activeEditor(); + if (activeEditor && !activeEditor.selection.isEmpty) { + return activeEditor.document.getText(activeEditor.selection); + } + } + return undefined; + }, + getLineNumber: (): string | undefined => { + if (editorService) { + const activeEditor = editorService.activeEditor(); + if (activeEditor) { + return String(activeEditor.selection.end.line + 1); + } + } + return undefined; + }, + getExtension: (id) => { + return extensionService.getExtension(id); + }, + }, undefined, homeDir ? Promise.resolve(homeDir) : undefined, Promise.resolve(process.env)); + } +} + +export class ExtHostVariableResolverProviderService extends Disposable implements IExtHostVariableResolverProvider { + declare readonly _serviceBrand: undefined; + + private _resolver = new Lazy(async () => { + const configProvider = await this.configurationService.getConfigProvider(); + const folders = await this.workspaceService.getWorkspaceFolders2() || []; + + const dynamic: DynamicContext = { folders }; + this._register(this.workspaceService.onDidChangeWorkspace(async e => { + dynamic.folders = await this.workspaceService.getWorkspaceFolders2() || []; + })); + + return new ExtHostVariableResolverService( + this.extensionService, + this.workspaceService, + this.editorService, + this.editorTabs, + configProvider, + dynamic, + this.homeDir(), + ); + }); + + constructor( + @IExtHostExtensionService private readonly extensionService: IExtHostExtensionService, + @IExtHostWorkspace private readonly workspaceService: IExtHostWorkspace, + @IExtHostDocumentsAndEditors private readonly editorService: IExtHostDocumentsAndEditors, + @IExtHostConfiguration private readonly configurationService: IExtHostConfiguration, + @IExtHostEditorTabs private readonly editorTabs: IExtHostEditorTabs, + ) { + super(); + } + + public getResolver(): Promise<IConfigurationResolverService> { + return this._resolver.getValue(); + } + + protected homeDir(): string | undefined { + return undefined; + } +} diff --git a/src/vs/workbench/api/common/extHostWebviewPanels.ts b/src/vs/workbench/api/common/extHostWebviewPanels.ts index 2f2e575ed76..189d350428b 100644 --- a/src/vs/workbench/api/common/extHostWebviewPanels.ts +++ b/src/vs/workbench/api/common/extHostWebviewPanels.ts @@ -32,7 +32,7 @@ class ExtHostWebviewPanel extends Disposable implements vscode.WebviewPanel { #iconPath?: IconPath; #viewColumn: vscode.ViewColumn | undefined = undefined; #visible: boolean = true; - #active: boolean = true; + #active: boolean; #isDisposed: boolean = false; readonly #onDidDispose = this._register(new Emitter<void>()); @@ -44,20 +44,24 @@ class ExtHostWebviewPanel extends Disposable implements vscode.WebviewPanel { constructor( handle: extHostProtocol.WebviewHandle, proxy: extHostProtocol.MainThreadWebviewPanelsShape, - viewType: string, - title: string, - viewColumn: vscode.ViewColumn | undefined, - panelOptions: vscode.WebviewPanelOptions, - webview: ExtHostWebview + webview: ExtHostWebview, + params: { + viewType: string; + title: string; + viewColumn: vscode.ViewColumn | undefined; + panelOptions: vscode.WebviewPanelOptions; + active: boolean; + } ) { super(); this.#handle = handle; this.#proxy = proxy; - this.#viewType = viewType; - this.#options = panelOptions; - this.#viewColumn = viewColumn; - this.#title = title; this.#webview = webview; + this.#viewType = params.viewType; + this.#options = params.panelOptions; + this.#viewColumn = params.viewColumn; + this.#title = params.title; + this.#active = params.active; } public override dispose() { @@ -209,7 +213,7 @@ export class ExtHostWebviewPanels implements extHostProtocol.ExtHostWebviewPanel }, webviewShowOptions); const webview = this.webviews.createNewWebview(handle, options, extension); - const panel = this.createNewWebviewPanel(handle, viewType, title, viewColumn, options, webview); + const panel = this.createNewWebviewPanel(handle, viewType, title, viewColumn, options, webview, true); return panel; } @@ -283,6 +287,7 @@ export class ExtHostWebviewPanels implements extHostProtocol.ExtHostWebviewPanel state: any; webviewOptions: extHostProtocol.IWebviewContentOptions; panelOptions: extHostProtocol.IWebviewPanelOptions; + active: boolean; }, position: EditorGroupColumn ): Promise<void> { @@ -293,12 +298,12 @@ export class ExtHostWebviewPanels implements extHostProtocol.ExtHostWebviewPanel const { serializer, extension } = entry; const webview = this.webviews.createNewWebview(webviewHandle, initData.webviewOptions, extension); - const revivedPanel = this.createNewWebviewPanel(webviewHandle, viewType, initData.title, position, initData.panelOptions, webview); + const revivedPanel = this.createNewWebviewPanel(webviewHandle, viewType, initData.title, position, initData.panelOptions, webview, initData.active); await serializer.deserializeWebviewPanel(revivedPanel, initData.state); } - public createNewWebviewPanel(webviewHandle: string, viewType: string, title: string, position: vscode.ViewColumn, options: extHostProtocol.IWebviewPanelOptions, webview: ExtHostWebview) { - const panel = new ExtHostWebviewPanel(webviewHandle, this._proxy, viewType, title, position, options, webview); + public createNewWebviewPanel(webviewHandle: string, viewType: string, title: string, position: vscode.ViewColumn, options: extHostProtocol.IWebviewPanelOptions, webview: ExtHostWebview, active: boolean) { + const panel = new ExtHostWebviewPanel(webviewHandle, this._proxy, webview, { viewType, title, viewColumn: position, panelOptions: options, active }); this._webviewPanels.set(webviewHandle, panel); return panel; } diff --git a/src/vs/workbench/api/common/extHostWebviewView.ts b/src/vs/workbench/api/common/extHostWebviewView.ts index 884e05f44a3..fbce7f6e892 100644 --- a/src/vs/workbench/api/common/extHostWebviewView.ts +++ b/src/vs/workbench/api/common/extHostWebviewView.ts @@ -7,7 +7,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { ExtHostWebview, ExtHostWebviews, toExtensionData } from 'vs/workbench/api/common/extHostWebview'; +import { ExtHostWebview, ExtHostWebviews, toExtensionData, shouldSerializeBuffersForPostMessage } from 'vs/workbench/api/common/extHostWebview'; import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import { ViewBadge } from 'vs/workbench/api/common/extHostTypeConverters'; import type * as vscode from 'vscode'; @@ -173,7 +173,7 @@ export class ExtHostWebviewViews implements extHostProtocol.ExtHostWebviewViewsS this._viewProviders.set(viewType, { provider, extension }); this._proxy.$registerWebviewViewProvider(toExtensionData(extension), viewType, { retainContextWhenHidden: webviewOptions?.retainContextWhenHidden, - serializeBuffersForPostMessage: false, + serializeBuffersForPostMessage: shouldSerializeBuffersForPostMessage(extension), }); return new extHostTypes.Disposable(() => { diff --git a/src/vs/workbench/api/common/extensionHostMain.ts b/src/vs/workbench/api/common/extensionHostMain.ts index 80f8d74b967..0eabc9e29cc 100644 --- a/src/vs/workbench/api/common/extensionHostMain.ts +++ b/src/vs/workbench/api/common/extensionHostMain.ts @@ -3,10 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { timeout } from 'vs/base/common/async'; import * as errors from 'vs/base/common/errors'; import * as performance from 'vs/base/common/performance'; -import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { IURITransformer } from 'vs/base/common/uriIpc'; import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; @@ -23,7 +21,6 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IExtHostRpcService, ExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { IURITransformerService, URITransformerService } from 'vs/workbench/api/common/extHostUriTransformerService'; import { IExtHostExtensionService, IHostUtils } from 'vs/workbench/api/common/extHostExtensionService'; -import { IExtHostTerminalService } from 'vs/workbench/api/common/extHostTerminalService'; export interface IExitFn { (code?: number): any; @@ -35,12 +32,10 @@ export interface IConsolePatchFn { export class ExtensionHostMain { - private _isTerminating: boolean; private readonly _hostUtils: IHostUtils; private readonly _rpcProtocol: RPCProtocol; private readonly _extensionService: IExtHostExtensionService; private readonly _logService: ILogService; - private readonly _disposables = new DisposableStore(); constructor( protocol: IMessagePassingProtocol, @@ -49,7 +44,6 @@ export class ExtensionHostMain { uriTransformer: IURITransformer | null, messagePorts?: ReadonlyMap<string, MessagePort> ) { - this._isTerminating = false; this._hostUtils = hostUtils; this._rpcProtocol = new RPCProtocol(protocol, null, uriTransformer); @@ -66,9 +60,6 @@ export class ExtensionHostMain { const instaService: IInstantiationService = new InstantiationService(services, true); // ugly self - inject - this._disposables.add(instaService.invokeFunction(accessor => accessor.get(IExtHostTerminalService))); - this._disposables.add(instaService.invokeFunction(accessor => accessor.get(IExtHostExtensionService))); - this._logService = instaService.invokeFunction(accessor => accessor.get(ILogService)); performance.mark(`code/extHost/didCreateServices`); @@ -125,40 +116,11 @@ export class ExtensionHostMain { } terminate(reason: string): void { - if (this._isTerminating) { - // we are already shutting down... - return; - } - this._isTerminating = true; - this._logService.info(`extension host terminating: ${reason}`); - this._logService.flush(); - - this._disposables.dispose(); - - errors.setUnexpectedErrorHandler((err) => { - this._logService.error(err); - }); - - // Invalidate all proxies - this._rpcProtocol.dispose(); - - const extensionsDeactivated = this._extensionService.deactivateAll(); - - // Give extensions at most 5 seconds to wrap up any async deactivate, then exit - Promise.race([timeout(5000), extensionsDeactivated]).finally(() => { - if (this._hostUtils.pid) { - this._logService.info(`Extension host with pid ${this._hostUtils.pid} exiting with code 0`); - } else { - this._logService.info(`Extension host exiting with code 0`); - } - this._logService.flush(); - this._logService.dispose(); - this._hostUtils.exit(0); - }); + this._extensionService.terminate(reason); } private static _transform(initData: IExtensionHostInitData, rpcProtocol: RPCProtocol): IExtensionHostInitData { - initData.extensions.forEach((ext) => (<any>ext).extensionLocation = URI.revive(rpcProtocol.transformIncomingURIs(ext.extensionLocation))); + initData.allExtensions.forEach((ext) => (<any>ext).extensionLocation = URI.revive(rpcProtocol.transformIncomingURIs(ext.extensionLocation))); initData.environment.appRoot = URI.revive(rpcProtocol.transformIncomingURIs(initData.environment.appRoot)); const extDevLocs = initData.environment.extensionDevelopmentLocationURI; if (extDevLocs) { diff --git a/src/vs/workbench/api/node/extHost.node.services.ts b/src/vs/workbench/api/node/extHost.node.services.ts index 5bc2abbca64..0cbd7e57c22 100644 --- a/src/vs/workbench/api/node/extHost.node.services.ts +++ b/src/vs/workbench/api/node/extHost.node.services.ts @@ -20,6 +20,8 @@ import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePa import { ExtensionStoragePaths } from 'vs/workbench/api/node/extHostStoragePaths'; import { ExtHostLoggerService } from 'vs/workbench/api/node/extHostLoggerService'; import { ILoggerService } from 'vs/platform/log/common/log'; +import { NodeExtHostVariableResolverProviderService } from 'vs/workbench/api/node/extHostVariableResolverService'; +import { IExtHostVariableResolverProvider } from 'vs/workbench/api/common/extHostVariableResolverService'; // ######################################################################### // ### ### @@ -36,3 +38,4 @@ registerSingleton(IExtHostSearch, NativeExtHostSearch); registerSingleton(IExtHostTask, ExtHostTask); registerSingleton(IExtHostTerminalService, ExtHostTerminalService); registerSingleton(IExtHostTunnelService, ExtHostTunnelService); +registerSingleton(IExtHostVariableResolverProvider, NodeExtHostVariableResolverProviderService); diff --git a/src/vs/workbench/api/node/extHostDebugService.ts b/src/vs/workbench/api/node/extHostDebugService.ts index b235b34a8fc..d2127334678 100644 --- a/src/vs/workbench/api/node/extHostDebugService.ts +++ b/src/vs/workbench/api/node/extHostDebugService.ts @@ -3,31 +3,29 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; -import type * as vscode from 'vscode'; -import { homedir } from 'os'; +import { createCancelablePromise, firstParallel } from 'vs/base/common/async'; +import { IDisposable } from 'vs/base/common/lifecycle'; import * as platform from 'vs/base/common/platform'; +import * as nls from 'vs/nls'; +import { IExternalTerminalService } from 'vs/platform/externalTerminal/common/externalTerminal'; +import { LinuxExternalTerminalService, MacExternalTerminalService, WindowsExternalTerminalService } from 'vs/platform/externalTerminal/node/externalTerminalService'; +import { ISignService } from 'vs/platform/sign/common/sign'; +import { SignService } from 'vs/platform/sign/node/signService'; +import { ExtHostDebugServiceBase, ExtHostDebugSession } from 'vs/workbench/api/common/extHostDebugService'; +import { IExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs'; +import { IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; +import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; +import { IExtHostTerminalService } from 'vs/workbench/api/common/extHostTerminalService'; import { DebugAdapterExecutable, ThemeIcon } from 'vs/workbench/api/common/extHostTypes'; -import { ExecutableDebugAdapter, SocketDebugAdapter, NamedPipeDebugAdapter } from 'vs/workbench/contrib/debug/node/debugAdapter'; -import { AbstractDebugAdapter } from 'vs/workbench/contrib/debug/common/abstractDebugAdapter'; +import { IExtHostVariableResolverProvider } from 'vs/workbench/api/common/extHostVariableResolverService'; import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; -import { IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; -import { IExtHostDocumentsAndEditors, ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; +import { AbstractDebugAdapter } from 'vs/workbench/contrib/debug/common/abstractDebugAdapter'; import { IAdapterDescriptor } from 'vs/workbench/contrib/debug/common/debug'; -import { IExtHostConfiguration, ExtHostConfigProvider } from '../common/extHostConfiguration'; -import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; -import { IExtHostTerminalService } from 'vs/workbench/api/common/extHostTerminalService'; -import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; -import { ExtHostDebugServiceBase, ExtHostDebugSession, ExtHostVariableResolverService } from 'vs/workbench/api/common/extHostDebugService'; -import { ISignService } from 'vs/platform/sign/common/sign'; -import { SignService } from 'vs/platform/sign/node/signService'; -import { IDisposable } from 'vs/base/common/lifecycle'; -import { AbstractVariableResolverService } from 'vs/workbench/services/configurationResolver/common/variableResolver'; -import { createCancelablePromise, firstParallel } from 'vs/base/common/async'; +import { ExecutableDebugAdapter, NamedPipeDebugAdapter, SocketDebugAdapter } from 'vs/workbench/contrib/debug/node/debugAdapter'; import { hasChildProcesses, prepareCommand } from 'vs/workbench/contrib/debug/node/terminals'; -import { IExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs'; -import { IExternalTerminalService } from 'vs/platform/externalTerminal/common/externalTerminal'; -import { WindowsExternalTerminalService, MacExternalTerminalService, LinuxExternalTerminalService } from 'vs/platform/externalTerminal/node/externalTerminalService'; +import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; +import type * as vscode from 'vscode'; +import { ExtHostConfigProvider, IExtHostConfiguration } from '../common/extHostConfiguration'; export class ExtHostDebugService extends ExtHostDebugServiceBase { @@ -40,12 +38,12 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { @IExtHostRpcService extHostRpcService: IExtHostRpcService, @IExtHostWorkspace workspaceService: IExtHostWorkspace, @IExtHostExtensionService extensionService: IExtHostExtensionService, - @IExtHostDocumentsAndEditors editorsService: IExtHostDocumentsAndEditors, @IExtHostConfiguration configurationService: IExtHostConfiguration, @IExtHostTerminalService private _terminalService: IExtHostTerminalService, - @IExtHostEditorTabs editorTabs: IExtHostEditorTabs + @IExtHostEditorTabs editorTabs: IExtHostEditorTabs, + @IExtHostVariableResolverProvider variableResolver: IExtHostVariableResolverProvider, ) { - super(extHostRpcService, workspaceService, extensionService, editorsService, configurationService, editorTabs); + super(extHostRpcService, workspaceService, extensionService, configurationService, editorTabs, variableResolver); } protected override createDebugAdapter(adapter: IAdapterDescriptor, session: ExtHostDebugSession): AbstractDebugAdapter | undefined { @@ -154,10 +152,6 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { } return super.$runInTerminal(args, sessionId); } - - protected createVariableResolver(folders: vscode.WorkspaceFolder[], editorService: ExtHostDocumentsAndEditors, configurationService: ExtHostConfigProvider): AbstractVariableResolverService { - return new ExtHostVariableResolverService(folders, editorService, configurationService, this._editorTabs, this._workspaceService, homedir()); - } } let externalTerminalService: IExternalTerminalService | undefined = undefined; diff --git a/src/vs/workbench/api/node/extHostExtensionService.ts b/src/vs/workbench/api/node/extHostExtensionService.ts index 481409b79d5..62e0d6b4998 100644 --- a/src/vs/workbench/api/node/extHostExtensionService.ts +++ b/src/vs/workbench/api/node/extHostExtensionService.ts @@ -72,7 +72,7 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { } // Module loading tricks - const interceptor = this._instaService.createInstance(NodeModuleRequireInterceptor, extensionApiFactory, this._registry); + const interceptor = this._instaService.createInstance(NodeModuleRequireInterceptor, extensionApiFactory, { mine: this._myRegistry, all: this._globalRegistry }); await interceptor.install(); performance.mark('code/extHost/didInitAPI'); diff --git a/src/vs/workbench/api/node/extHostTask.ts b/src/vs/workbench/api/node/extHostTask.ts index c47e3203461..7afdfc40ea0 100644 --- a/src/vs/workbench/api/node/extHostTask.ts +++ b/src/vs/workbench/api/node/extHostTask.ts @@ -11,7 +11,6 @@ import * as types from 'vs/workbench/api/common/extHostTypes'; import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; import type * as vscode from 'vscode'; import * as tasks from '../common/shared/tasks'; -import { ExtHostVariableResolverService } from 'vs/workbench/api/common/extHostDebugService'; import { IExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { IExtHostConfiguration } from 'vs/workbench/api/common/extHostConfiguration'; import { IWorkspaceFolder, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; @@ -23,13 +22,11 @@ import { ExtHostTaskBase, TaskHandleDTO, TaskDTO, CustomExecutionDTO, HandlerDat import { Schemas } from 'vs/base/common/network'; import { ILogService } from 'vs/platform/log/common/log'; import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; -import { IExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs'; import * as resources from 'vs/base/common/resources'; import { homedir } from 'os'; +import { IExtHostVariableResolverProvider } from 'vs/workbench/api/common/extHostVariableResolverService'; export class ExtHostTask extends ExtHostTaskBase { - private _variableResolver: ExtHostVariableResolverService | undefined; - constructor( @IExtHostRpcService extHostRpc: IExtHostRpcService, @IExtHostInitDataService initData: IExtHostInitDataService, @@ -39,7 +36,7 @@ export class ExtHostTask extends ExtHostTaskBase { @IExtHostTerminalService extHostTerminalService: IExtHostTerminalService, @ILogService logService: ILogService, @IExtHostApiDeprecationService deprecationService: IExtHostApiDeprecationService, - @IExtHostEditorTabs private readonly editorTabs: IExtHostEditorTabs, + @IExtHostVariableResolverProvider private readonly variableResolver: IExtHostVariableResolverProvider, ) { super(extHostRpc, initData, workspaceService, editorService, configurationService, extHostTerminalService, logService, deprecationService); if (initData.remote.isRemote && initData.remote.authority) { @@ -128,14 +125,6 @@ export class ExtHostTask extends ExtHostTaskBase { return resolvedTaskDTO; } - private async getVariableResolver(workspaceFolders: vscode.WorkspaceFolder[]): Promise<ExtHostVariableResolverService> { - if (this._variableResolver === undefined) { - const configProvider = await this._configurationService.getConfigProvider(); - this._variableResolver = new ExtHostVariableResolverService(workspaceFolders, this._editorService, configProvider, this.editorTabs, this.workspaceService, homedir()); - } - return this._variableResolver; - } - private async getAFolder(workspaceFolders: vscode.WorkspaceFolder[] | undefined): Promise<IWorkspaceFolder> { let folder = (workspaceFolders && workspaceFolders.length > 0) ? workspaceFolders[0] : undefined; if (!folder) { @@ -161,7 +150,7 @@ export class ExtHostTask extends ExtHostTaskBase { const workspaceFolder = await this._workspaceProvider.resolveWorkspaceFolder(uri); const workspaceFolders = (await this._workspaceProvider.getWorkspaceFolders2()) ?? []; - const resolver = await this.getVariableResolver(workspaceFolders); + const resolver = await this.variableResolver.getResolver(); const ws: IWorkspaceFolder = workspaceFolder ? { uri: workspaceFolder.uri, name: workspaceFolder.name, diff --git a/src/vs/workbench/api/node/extHostVariableResolverService.ts b/src/vs/workbench/api/node/extHostVariableResolverService.ts new file mode 100644 index 00000000000..c6439fd4e03 --- /dev/null +++ b/src/vs/workbench/api/node/extHostVariableResolverService.ts @@ -0,0 +1,13 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { homedir } from 'os'; +import { ExtHostVariableResolverProviderService } from 'vs/workbench/api/common/extHostVariableResolverService'; + +export class NodeExtHostVariableResolverProviderService extends ExtHostVariableResolverProviderService { + protected override homeDir(): string | undefined { + return homedir(); + } +} diff --git a/src/vs/workbench/api/test/browser/extHostAuthentication.test.ts b/src/vs/workbench/api/test/browser/extHostAuthentication.test.ts index 0a9919154a3..c8dd0d0f7c6 100644 --- a/src/vs/workbench/api/test/browser/extHostAuthentication.test.ts +++ b/src/vs/workbench/api/test/browser/extHostAuthentication.test.ts @@ -22,9 +22,8 @@ import { AuthenticationService } from 'vs/workbench/services/authentication/brow import { IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; import { IExtensionService, nullExtensionDescription as extensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; -import { TestRemoteAgentService } from 'vs/workbench/services/remote/test/common/testServices'; import { TestRPCProtocol } from 'vs/workbench/api/test/common/testRPCProtocol'; -import { TestQuickInputService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestQuickInputService, TestRemoteAgentService } from 'vs/workbench/test/browser/workbenchTestServices'; import { TestActivityService, TestExtensionService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; import type { AuthenticationProvider, AuthenticationSession } from 'vscode'; diff --git a/src/vs/workbench/api/test/browser/extHostDiagnostics.test.ts b/src/vs/workbench/api/test/browser/extHostDiagnostics.test.ts index 6b4487af06e..32ccfd141ff 100644 --- a/src/vs/workbench/api/test/browser/extHostDiagnostics.test.ts +++ b/src/vs/workbench/api/test/browser/extHostDiagnostics.test.ts @@ -392,6 +392,7 @@ suite('ExtHostDiagnostics', () => { set(): any { return null; } + dispose() { } assertRegistered(): void { } @@ -444,6 +445,7 @@ suite('ExtHostDiagnostics', () => { set(): any { return null; } + dispose() { } assertRegistered(): void { } @@ -484,6 +486,7 @@ suite('ExtHostDiagnostics', () => { set(): any { return null; } + dispose() { } assertRegistered(): void { } diff --git a/src/vs/workbench/api/test/browser/extHostEditorTabs.test.ts b/src/vs/workbench/api/test/browser/extHostEditorTabs.test.ts index a9ff3908319..1e385fc213f 100644 --- a/src/vs/workbench/api/test/browser/extHostEditorTabs.test.ts +++ b/src/vs/workbench/api/test/browser/extHostEditorTabs.test.ts @@ -7,7 +7,7 @@ import type * as vscode from 'vscode'; import assert = require('assert'); import { URI } from 'vs/base/common/uri'; import { mock } from 'vs/base/test/common/mock'; -import { IEditorTabDto, MainThreadEditorTabsShape, TabInputKind, TabModelOperationKind, TextInputDto } from 'vs/workbench/api/common/extHost.protocol'; +import { IEditorTabDto, IEditorTabGroupDto, MainThreadEditorTabsShape, TabInputKind, TabModelOperationKind, TextInputDto } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs'; import { SingleProxyRPCProtocol } from 'vs/workbench/api/test/common/testRPCProtocol'; import { TextTabInput } from 'vs/workbench/api/common/extHostTypes'; @@ -35,7 +35,7 @@ suite('ExtHostEditorTabs', function () { }) ); - assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 0); + assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 0); // Active group should never be undefined (there is always an active group). Ensure accessing it undefined throws. // TODO @lramos15 Add a throw on the main side when a model is sent without an active group assert.throws(() => extHostEditorTabs.tabGroups.activeTabGroup); @@ -63,8 +63,8 @@ suite('ExtHostEditorTabs', function () { groupId: 12, tabs: [tab] }]); - assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 1); - const [first] = extHostEditorTabs.tabGroups.groups; + assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1); + const [first] = extHostEditorTabs.tabGroups.all; assert.ok(first.activeTab); assert.strictEqual(first.tabs.indexOf(first.activeTab), 0); @@ -75,8 +75,8 @@ suite('ExtHostEditorTabs', function () { groupId: 12, tabs: [tab] }]); - assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 1); - const [first] = extHostEditorTabs.tabGroups.groups; + assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1); + const [first] = extHostEditorTabs.tabGroups.all; assert.ok(first.activeTab); assert.strictEqual(first.tabs.indexOf(first.activeTab), 0); } @@ -95,8 +95,8 @@ suite('ExtHostEditorTabs', function () { groupId: 12, tabs: [] }]); - assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 1); - const [first] = extHostEditorTabs.tabGroups.groups; + assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1); + const [first] = extHostEditorTabs.tabGroups.all; assert.strictEqual(first.activeTab, undefined); assert.strictEqual(first.tabs.length, 0); }); @@ -121,11 +121,65 @@ suite('ExtHostEditorTabs', function () { }]); assert.ok(extHostEditorTabs.tabGroups.activeTabGroup); const activeTabGroup: vscode.TabGroup = extHostEditorTabs.tabGroups.activeTabGroup; - assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 1); + assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1); assert.strictEqual(activeTabGroup.tabs.length, 0); assert.strictEqual(count, 1); }); + test('Check TabGroupChangeEvent properties', function () { + const extHostEditorTabs = new ExtHostEditorTabs( + SingleProxyRPCProtocol(new class extends mock<MainThreadEditorTabsShape>() { + // override/implement $moveTab or $closeTab + }) + ); + + const group1Data: IEditorTabGroupDto = { + isActive: true, + viewColumn: 0, + groupId: 12, + tabs: [] + }; + const group2Data: IEditorTabGroupDto = { ...group1Data, groupId: 13 }; + + const events: vscode.TabGroupChangeEvent[] = []; + extHostEditorTabs.tabGroups.onDidChangeTabGroups(e => events.push(e)); + // OPEN + extHostEditorTabs.$acceptEditorTabModel([group1Data]); + assert.deepStrictEqual(events, [{ + changed: [], + closed: [], + opened: [extHostEditorTabs.tabGroups.activeTabGroup] + }]); + + // OPEN, CHANGE + events.length = 0; + extHostEditorTabs.$acceptEditorTabModel([{ ...group1Data, isActive: false }, group2Data]); + assert.deepStrictEqual(events, [{ + changed: [extHostEditorTabs.tabGroups.all[0]], + closed: [], + opened: [extHostEditorTabs.tabGroups.all[1]] + }]); + + // CHANGE + events.length = 0; + extHostEditorTabs.$acceptEditorTabModel([group1Data, { ...group2Data, isActive: false }]); + assert.deepStrictEqual(events, [{ + changed: extHostEditorTabs.tabGroups.all, + closed: [], + opened: [] + }]); + + // CLOSE, CHANGE + events.length = 0; + const oldActiveGroup = extHostEditorTabs.tabGroups.activeTabGroup; + extHostEditorTabs.$acceptEditorTabModel([group2Data]); + assert.deepStrictEqual(events, [{ + changed: extHostEditorTabs.tabGroups.all, + closed: [oldActiveGroup], + opened: [] + }]); + }); + test('Ensure reference equality for activeTab and activeGroup', function () { const extHostEditorTabs = new ExtHostEditorTabs( SingleProxyRPCProtocol(new class extends mock<MainThreadEditorTabsShape>() { @@ -147,8 +201,8 @@ suite('ExtHostEditorTabs', function () { groupId: 12, tabs: [tab] }]); - assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 1); - const [first] = extHostEditorTabs.tabGroups.groups; + assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1); + const [first] = extHostEditorTabs.tabGroups.all; assert.ok(first.activeTab); assert.strictEqual(first.tabs.indexOf(first.activeTab), 0); assert.strictEqual(first.activeTab, first.tabs[0]); @@ -172,13 +226,13 @@ suite('ExtHostEditorTabs', function () { groupId: 12, tabs: [tabDto] }]); - let all = extHostEditorTabs.tabGroups.groups.map(group => group.tabs).flat(); + let all = extHostEditorTabs.tabGroups.all.map(group => group.tabs).flat(); assert.strictEqual(all.length, 1); const apiTab1 = all[0]; - assert.ok(apiTab1.kind instanceof TextTabInput); + assert.ok(apiTab1.input instanceof TextTabInput); assert.strictEqual(tabDto.input.kind, TabInputKind.TextInput); const dtoResource = (tabDto.input as TextInputDto).uri; - assert.strictEqual(apiTab1.kind.uri.toString(), URI.revive(dtoResource).toString()); + assert.strictEqual(apiTab1.input.uri.toString(), URI.revive(dtoResource).toString()); assert.strictEqual(apiTab1.isDirty, true); @@ -193,11 +247,11 @@ suite('ExtHostEditorTabs', function () { groupId: 12 }); - all = extHostEditorTabs.tabGroups.groups.map(group => group.tabs).flat(); + all = extHostEditorTabs.tabGroups.all.map(group => group.tabs).flat(); assert.strictEqual(all.length, 1); const apiTab2 = all[0]; - assert.ok(apiTab1.kind instanceof TextTabInput); - assert.strictEqual(apiTab1.kind.uri.toString(), URI.revive(dtoResource).toString()); + assert.ok(apiTab1.input instanceof TextTabInput); + assert.strictEqual(apiTab1.input.uri.toString(), URI.revive(dtoResource).toString()); assert.strictEqual(apiTab2.isDirty, false); assert.strictEqual(apiTab1 === apiTab2, true); @@ -239,14 +293,14 @@ suite('ExtHostEditorTabs', function () { tabs: [tabDtoAAA, tabDtoBBB] }]); - let all = extHostEditorTabs.tabGroups.groups.map(group => group.tabs).flat(); + let all = extHostEditorTabs.tabGroups.all.map(group => group.tabs).flat(); assert.strictEqual(all.length, 2); const activeTab1 = extHostEditorTabs.tabGroups.activeTabGroup?.activeTab; - assert.ok(activeTab1?.kind instanceof TextTabInput); + assert.ok(activeTab1?.input instanceof TextTabInput); assert.strictEqual(tabDtoAAA.input.kind, TabInputKind.TextInput); const dtoAAAResource = (tabDtoAAA.input as TextInputDto).uri; - assert.strictEqual(activeTab1?.kind?.uri.toString(), URI.revive(dtoAAAResource)?.toString()); + assert.strictEqual(activeTab1?.input?.uri.toString(), URI.revive(dtoAAAResource)?.toString()); assert.strictEqual(activeTab1?.isActive, true); extHostEditorTabs.$acceptTabOperation({ @@ -257,10 +311,10 @@ suite('ExtHostEditorTabs', function () { }); const activeTab2 = extHostEditorTabs.tabGroups.activeTabGroup?.activeTab; - assert.ok(activeTab2?.kind instanceof TextTabInput); + assert.ok(activeTab2?.input instanceof TextTabInput); assert.strictEqual(tabDtoBBB.input.kind, TabInputKind.TextInput); const dtoBBBResource = (tabDtoBBB.input as TextInputDto).uri; - assert.strictEqual(activeTab2?.kind?.uri.toString(), URI.revive(dtoBBBResource)?.toString()); + assert.strictEqual(activeTab2?.input?.uri.toString(), URI.revive(dtoBBBResource)?.toString()); assert.strictEqual(activeTab2?.isActive, true); assert.strictEqual(activeTab1?.isActive, false); }); @@ -279,7 +333,7 @@ suite('ExtHostEditorTabs', function () { }); assert.throws(() => { // @ts-expect-error write to readonly prop - extHostEditorTabs.tabGroups.groups.length = 0; + extHostEditorTabs.tabGroups.all.length = 0; }); assert.throws(() => { // @ts-expect-error write to readonly prop @@ -317,7 +371,7 @@ suite('ExtHostEditorTabs', function () { groupId: 12, tabs: [tab] }]); - assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 1); + assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1); const activeTab = extHostEditorTabs.tabGroups.activeTabGroup?.activeTab; assert.ok(activeTab); extHostEditorTabs.tabGroups.close(activeTab, false); @@ -356,12 +410,12 @@ suite('ExtHostEditorTabs', function () { tabs: [tabDto] }]); - assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 1); - assert.strictEqual(extHostEditorTabs.tabGroups.groups.map(g => g.tabs).flat().length, 1); + assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1); + assert.strictEqual(extHostEditorTabs.tabGroups.all.map(g => g.tabs).flat().length, 1); - const tab = extHostEditorTabs.tabGroups.groups[0].tabs[0]; + const tab = extHostEditorTabs.tabGroups.all[0].tabs[0]; - const p = new Promise<vscode.Tab[]>(resolve => extHostEditorTabs.tabGroups.onDidChangeTabs(resolve)); + const p = new Promise<vscode.TabChangeEvent>(resolve => extHostEditorTabs.tabGroups.onDidChangeTabs(resolve)); extHostEditorTabs.$acceptTabOperation({ groupId: 12, @@ -370,7 +424,7 @@ suite('ExtHostEditorTabs', function () { tabDto: { ...tabDto, label: 'NEW LABEL' } }); - const changedTab = (await p)[0]; + const changedTab = (await p).changed[0]; assert.ok(tab === changedTab); assert.strictEqual(changedTab.label, 'NEW LABEL'); @@ -410,8 +464,8 @@ suite('ExtHostEditorTabs', function () { tabs: [tab1, tab2, tab3] }]); - assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 1); - assert.strictEqual(extHostEditorTabs.tabGroups.groups.map(g => g.tabs).flat().length, 3); + assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1); + assert.strictEqual(extHostEditorTabs.tabGroups.all.map(g => g.tabs).flat().length, 3); // Active tab is correct assert.strictEqual(extHostEditorTabs.tabGroups.activeTabGroup?.activeTab, extHostEditorTabs.tabGroups.activeTabGroup?.tabs[0]); @@ -441,8 +495,8 @@ suite('ExtHostEditorTabs', function () { groupId: 12, tabs: [tab3] }]); - assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 1); - assert.strictEqual(extHostEditorTabs.tabGroups.groups.map(g => g.tabs).flat().length, 1); + assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1); + assert.strictEqual(extHostEditorTabs.tabGroups.all.map(g => g.tabs).flat().length, 1); assert.strictEqual(extHostEditorTabs.tabGroups.activeTabGroup?.activeTab, extHostEditorTabs.tabGroups.activeTabGroup?.tabs[0]); // Closing out all tabs returns undefine active tab @@ -452,8 +506,8 @@ suite('ExtHostEditorTabs', function () { groupId: 12, tabs: [] }]); - assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 1); - assert.strictEqual(extHostEditorTabs.tabGroups.groups.map(g => g.tabs).flat().length, 0); + assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1); + assert.strictEqual(extHostEditorTabs.tabGroups.all.map(g => g.tabs).flat().length, 0); assert.strictEqual(extHostEditorTabs.tabGroups.activeTabGroup?.activeTab, undefined); }); @@ -489,8 +543,8 @@ suite('ExtHostEditorTabs', function () { tabs: [tab1, tab2, tab3] }]); - assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 1); - assert.strictEqual(extHostEditorTabs.tabGroups.groups.map(g => g.tabs).flat().length, 3); + assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1); + assert.strictEqual(extHostEditorTabs.tabGroups.all.map(g => g.tabs).flat().length, 3); // Close tab 2 extHostEditorTabs.$acceptTabOperation({ @@ -499,8 +553,8 @@ suite('ExtHostEditorTabs', function () { kind: TabModelOperationKind.TAB_CLOSE, tabDto: tab2 }); - assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 1); - assert.strictEqual(extHostEditorTabs.tabGroups.groups.map(g => g.tabs).flat().length, 2); + assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1); + assert.strictEqual(extHostEditorTabs.tabGroups.all.map(g => g.tabs).flat().length, 2); // Close active tab and update tab 3 to be active extHostEditorTabs.$acceptTabOperation({ @@ -509,8 +563,8 @@ suite('ExtHostEditorTabs', function () { kind: TabModelOperationKind.TAB_CLOSE, tabDto: tab1 }); - assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 1); - assert.strictEqual(extHostEditorTabs.tabGroups.groups.map(g => g.tabs).flat().length, 1); + assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1); + assert.strictEqual(extHostEditorTabs.tabGroups.all.map(g => g.tabs).flat().length, 1); tab3.isActive = true; extHostEditorTabs.$acceptTabOperation({ groupId: 12, @@ -518,9 +572,9 @@ suite('ExtHostEditorTabs', function () { kind: TabModelOperationKind.TAB_UPDATE, tabDto: tab3 }); - assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 1); - assert.strictEqual(extHostEditorTabs.tabGroups.groups.map(g => g.tabs).flat().length, 1); - assert.strictEqual(extHostEditorTabs.tabGroups.groups[0]?.activeTab?.label, 'label3'); + assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1); + assert.strictEqual(extHostEditorTabs.tabGroups.all.map(g => g.tabs).flat().length, 1); + assert.strictEqual(extHostEditorTabs.tabGroups.all[0]?.activeTab?.label, 'label3'); // Open tab 2 back extHostEditorTabs.$acceptTabOperation({ @@ -529,8 +583,70 @@ suite('ExtHostEditorTabs', function () { kind: TabModelOperationKind.TAB_OPEN, tabDto: tab2 }); - assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 1); - assert.strictEqual(extHostEditorTabs.tabGroups.groups.map(g => g.tabs).flat().length, 2); - assert.strictEqual(extHostEditorTabs.tabGroups.groups[0]?.tabs[1]?.label, 'label2'); + assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1); + assert.strictEqual(extHostEditorTabs.tabGroups.all.map(g => g.tabs).flat().length, 2); + assert.strictEqual(extHostEditorTabs.tabGroups.all[0]?.tabs[1]?.label, 'label2'); + }); + + test('Tab operations patches move correctly', function () { + const extHostEditorTabs = new ExtHostEditorTabs( + SingleProxyRPCProtocol(new class extends mock<MainThreadEditorTabsShape>() { + // override/implement $moveTab or $closeTab + }) + ); + + const tab1: IEditorTabDto = createTabDto({ + id: 'uniquestring', + isActive: true, + label: 'label1', + }); + + const tab2: IEditorTabDto = createTabDto({ + isActive: false, + id: 'uniquestring2', + label: 'label2', + }); + + const tab3: IEditorTabDto = createTabDto({ + isActive: false, + id: 'uniquestring3', + label: 'label3', + }); + + extHostEditorTabs.$acceptEditorTabModel([{ + isActive: true, + viewColumn: 0, + groupId: 12, + tabs: [tab1, tab2, tab3] + }]); + + assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1); + assert.strictEqual(extHostEditorTabs.tabGroups.all.map(g => g.tabs).flat().length, 3); + + // Move tab 2 to index 0 + extHostEditorTabs.$acceptTabOperation({ + groupId: 12, + index: 0, + oldIndex: 1, + kind: TabModelOperationKind.TAB_MOVE, + tabDto: tab2 + }); + assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1); + assert.strictEqual(extHostEditorTabs.tabGroups.all.map(g => g.tabs).flat().length, 3); + assert.strictEqual(extHostEditorTabs.tabGroups.all[0]?.tabs[0]?.label, 'label2'); + + // Move tab 3 to index 1 + extHostEditorTabs.$acceptTabOperation({ + groupId: 12, + index: 1, + oldIndex: 2, + kind: TabModelOperationKind.TAB_MOVE, + tabDto: tab3 + }); + assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1); + assert.strictEqual(extHostEditorTabs.tabGroups.all.map(g => g.tabs).flat().length, 3); + assert.strictEqual(extHostEditorTabs.tabGroups.all[0]?.tabs[1]?.label, 'label3'); + assert.strictEqual(extHostEditorTabs.tabGroups.all[0]?.tabs[0]?.label, 'label2'); + assert.strictEqual(extHostEditorTabs.tabGroups.all[0]?.tabs[2]?.label, 'label1'); }); }); diff --git a/src/vs/workbench/api/test/browser/extHostFileSystemEventService.test.ts b/src/vs/workbench/api/test/browser/extHostFileSystemEventService.test.ts index 110fa1c2b6f..64d35e85b79 100644 --- a/src/vs/workbench/api/test/browser/extHostFileSystemEventService.test.ts +++ b/src/vs/workbench/api/test/browser/extHostFileSystemEventService.test.ts @@ -14,6 +14,7 @@ suite('ExtHostFileSystemEventService', () => { const protocol: IMainContext = { getProxy: () => { return undefined!; }, set: undefined!, + dispose: undefined!, assertRegistered: undefined!, drain: undefined! }; diff --git a/src/vs/workbench/api/test/browser/extHostNotebookConcatDocument.test.ts b/src/vs/workbench/api/test/browser/extHostNotebookConcatDocument.test.ts deleted file mode 100644 index 6267d249941..00000000000 --- a/src/vs/workbench/api/test/browser/extHostNotebookConcatDocument.test.ts +++ /dev/null @@ -1,620 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as assert from 'assert'; -import { TestRPCProtocol } from 'vs/workbench/api/test/common/testRPCProtocol'; -import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; -import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; -import { NullLogService } from 'vs/platform/log/common/log'; -import { ExtHostNotebookConcatDocument } from 'vs/workbench/api/common/extHostNotebookConcatDocument'; -import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; -import { ExtHostNotebookDocument } from 'vs/workbench/api/common/extHostNotebookDocument'; -import { URI } from 'vs/base/common/uri'; -import { CellKind, CellUri, NotebookCellsChangeType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { Position, Location, Range } from 'vs/workbench/api/common/extHostTypes'; -import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; -import { nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; -import * as vscode from 'vscode'; -import { mock } from 'vs/workbench/test/common/workbenchTestServices'; -import { MainContext, MainThreadCommandsShape, MainThreadNotebookShape } from 'vs/workbench/api/common/extHost.protocol'; -import { DisposableStore } from 'vs/base/common/lifecycle'; -import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths'; -import { generateUuid } from 'vs/base/common/uuid'; -import { ExtHostNotebookDocuments } from 'vs/workbench/api/common/extHostNotebookDocuments'; -import { SerializableObjectWithBuffers } from 'vs/workbench/services/extensions/common/proxyIdentifier'; - -suite('NotebookConcatDocument', function () { - - let rpcProtocol: TestRPCProtocol; - let notebook: ExtHostNotebookDocument; - let extHostDocumentsAndEditors: ExtHostDocumentsAndEditors; - let extHostDocuments: ExtHostDocuments; - let extHostNotebooks: ExtHostNotebookController; - let extHostNotebookDocuments: ExtHostNotebookDocuments; - - const notebookUri = URI.parse('test:///notebook.file'); - const disposables = new DisposableStore(); - - setup(async function () { - disposables.clear(); - - rpcProtocol = new TestRPCProtocol(); - rpcProtocol.set(MainContext.MainThreadCommands, new class extends mock<MainThreadCommandsShape>() { - override $registerCommand() { } - }); - rpcProtocol.set(MainContext.MainThreadNotebook, new class extends mock<MainThreadNotebookShape>() { - override async $registerNotebookProvider() { } - override async $unregisterNotebookProvider() { } - }); - extHostDocumentsAndEditors = new ExtHostDocumentsAndEditors(rpcProtocol, new NullLogService()); - extHostDocuments = new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors); - const extHostStoragePaths = new class extends mock<IExtensionStoragePaths>() { - override workspaceValue() { - return URI.from({ scheme: 'test', path: generateUuid() }); - } - }; - extHostNotebooks = new ExtHostNotebookController(rpcProtocol, new ExtHostCommands(rpcProtocol, new NullLogService()), extHostDocumentsAndEditors, extHostDocuments, extHostStoragePaths); - extHostNotebookDocuments = new ExtHostNotebookDocuments(extHostNotebooks); - - let reg = extHostNotebooks.registerNotebookContentProvider(nullExtensionDescription, 'test', new class extends mock<vscode.NotebookContentProvider>() { - // async openNotebook() { } - }); - extHostNotebooks.$acceptDocumentAndEditorsDelta(new SerializableObjectWithBuffers({ - addedDocuments: [{ - uri: notebookUri, - viewType: 'test', - cells: [{ - handle: 0, - uri: CellUri.generate(notebookUri, 0), - source: ['### Heading'], - eol: '\n', - language: 'markdown', - cellKind: CellKind.Markup, - outputs: [], - }], - versionId: 0 - }], - addedEditors: [{ - documentUri: notebookUri, - id: '_notebook_editor_0', - selections: [{ start: 0, end: 1 }], - visibleRanges: [] - }] - })); - extHostNotebooks.$acceptDocumentAndEditorsDelta(new SerializableObjectWithBuffers({ newActiveEditor: '_notebook_editor_0' })); - - notebook = extHostNotebooks.notebookDocuments[0]!; - - disposables.add(reg); - disposables.add(notebook); - disposables.add(extHostDocuments); - }); - - test('empty', function () { - let doc = new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook.apiNotebook, undefined); - assert.strictEqual(doc.getText(), ''); - assert.strictEqual(doc.version, 0); - - // assert.strictEqual(doc.locationAt(new Position(0, 0)), undefined); - // assert.strictEqual(doc.positionAt(SOME_FAKE_LOCATION?), undefined); - }); - - - function assertLocation(doc: vscode.NotebookConcatTextDocument, pos: Position, expected: Location, reverse = true) { - const actual = doc.locationAt(pos); - assert.strictEqual(actual.uri.toString(), expected.uri.toString()); - assert.strictEqual(actual.range.isEqual(expected.range), true); - - if (reverse) { - // reverse - offset - const offset = doc.offsetAt(pos); - assert.strictEqual(doc.positionAt(offset).isEqual(pos), true); - - // reverse - pos - const actualPosition = doc.positionAt(actual); - assert.strictEqual(actualPosition.isEqual(pos), true); - } - } - - function assertLines(doc: vscode.NotebookConcatTextDocument, ...lines: string[]) { - let actual = doc.getText().split(/\r\n|\n|\r/); - assert.deepStrictEqual(actual, lines); - } - - test('contains', function () { - - const cellUri1 = CellUri.generate(notebook.uri, 1); - const cellUri2 = CellUri.generate(notebook.uri, 2); - - extHostNotebookDocuments.$acceptModelChanged(notebookUri, new SerializableObjectWithBuffers({ - versionId: notebook.apiNotebook.version + 1, - rawEvents: [{ - kind: NotebookCellsChangeType.ModelChange, - changes: [[0, 0, [{ - handle: 1, - uri: cellUri1, - source: ['Hello', 'World', 'Hello World!'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }, { - handle: 2, - uri: cellUri2, - source: ['Hallo', 'Welt', 'Hallo Welt!'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }]] - ] - }] - }), false); - - - assert.strictEqual(notebook.apiNotebook.cellCount, 1 + 2); // markdown and code - - let doc = new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook.apiNotebook, undefined); - - assert.strictEqual(doc.contains(cellUri1), true); - assert.strictEqual(doc.contains(cellUri2), true); - assert.strictEqual(doc.contains(URI.parse('some://miss/path')), false); - }); - - test('location, position mapping', function () { - - extHostNotebookDocuments.$acceptModelChanged(notebookUri, new SerializableObjectWithBuffers({ - versionId: notebook.apiNotebook.version + 1, - rawEvents: [ - { - kind: NotebookCellsChangeType.ModelChange, - changes: [[0, 0, [{ - handle: 1, - uri: CellUri.generate(notebook.uri, 1), - source: ['Hello', 'World', 'Hello World!'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }, { - handle: 2, - uri: CellUri.generate(notebook.uri, 2), - source: ['Hallo', 'Welt', 'Hallo Welt!'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }]]] - } - ] - }), false); - - - assert.strictEqual(notebook.apiNotebook.cellCount, 1 + 2); // markdown and code - - let doc = new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook.apiNotebook, undefined); - assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); - - assertLocation(doc, new Position(0, 0), new Location(notebook.apiNotebook.cellAt(0).document.uri, new Position(0, 0))); - assertLocation(doc, new Position(4, 0), new Location(notebook.apiNotebook.cellAt(1).document.uri, new Position(1, 0))); - assertLocation(doc, new Position(4, 3), new Location(notebook.apiNotebook.cellAt(1).document.uri, new Position(1, 3))); - assertLocation(doc, new Position(5, 11), new Location(notebook.apiNotebook.cellAt(1).document.uri, new Position(2, 11))); - assertLocation(doc, new Position(5, 12), new Location(notebook.apiNotebook.cellAt(1).document.uri, new Position(2, 11)), false); // don't check identity because position will be clamped - }); - - - test('location, position mapping, cell changes', function () { - - let doc = new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook.apiNotebook, undefined); - - // UPDATE 1 - extHostNotebookDocuments.$acceptModelChanged(notebookUri, new SerializableObjectWithBuffers({ - versionId: notebook.apiNotebook.version + 1, - rawEvents: [ - { - kind: NotebookCellsChangeType.ModelChange, - changes: [[0, 0, [{ - handle: 1, - uri: CellUri.generate(notebook.uri, 1), - source: ['Hello', 'World', 'Hello World!'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }]]] - } - ] - }), false); - assert.strictEqual(notebook.apiNotebook.cellCount, 1 + 1); - assert.strictEqual(doc.version, 1); - assertLines(doc, 'Hello', 'World', 'Hello World!'); - - assertLocation(doc, new Position(0, 0), new Location(notebook.apiNotebook.cellAt(0).document.uri, new Position(0, 0))); - assertLocation(doc, new Position(2, 2), new Location(notebook.apiNotebook.cellAt(0).document.uri, new Position(2, 2))); - assertLocation(doc, new Position(4, 0), new Location(notebook.apiNotebook.cellAt(0).document.uri, new Position(2, 12)), false); // clamped - - - // UPDATE 2 - extHostNotebookDocuments.$acceptModelChanged(notebookUri, new SerializableObjectWithBuffers({ - versionId: notebook.apiNotebook.version + 1, - rawEvents: [ - { - kind: NotebookCellsChangeType.ModelChange, - changes: [[1, 0, [{ - handle: 2, - uri: CellUri.generate(notebook.uri, 2), - source: ['Hallo', 'Welt', 'Hallo Welt!'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }]]] - } - ] - }), false); - - assert.strictEqual(notebook.apiNotebook.cellCount, 1 + 2); - assert.strictEqual(doc.version, 2); - assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); - assertLocation(doc, new Position(0, 0), new Location(notebook.apiNotebook.cellAt(0).document.uri, new Position(0, 0))); - assertLocation(doc, new Position(4, 0), new Location(notebook.apiNotebook.cellAt(1).document.uri, new Position(1, 0))); - assertLocation(doc, new Position(4, 3), new Location(notebook.apiNotebook.cellAt(1).document.uri, new Position(1, 3))); - assertLocation(doc, new Position(5, 11), new Location(notebook.apiNotebook.cellAt(1).document.uri, new Position(2, 11))); - assertLocation(doc, new Position(5, 12), new Location(notebook.apiNotebook.cellAt(1).document.uri, new Position(2, 11)), false); // don't check identity because position will be clamped - - // UPDATE 3 (remove cell #2 again) - extHostNotebookDocuments.$acceptModelChanged(notebookUri, new SerializableObjectWithBuffers({ - versionId: notebook.apiNotebook.version + 1, - rawEvents: [ - { - kind: NotebookCellsChangeType.ModelChange, - changes: [[1, 1, []]] - } - ] - }), false); - assert.strictEqual(notebook.apiNotebook.cellCount, 1 + 1); - assert.strictEqual(doc.version, 3); - assertLines(doc, 'Hello', 'World', 'Hello World!'); - assertLocation(doc, new Position(0, 0), new Location(notebook.apiNotebook.cellAt(0).document.uri, new Position(0, 0))); - assertLocation(doc, new Position(2, 2), new Location(notebook.apiNotebook.cellAt(0).document.uri, new Position(2, 2))); - assertLocation(doc, new Position(4, 0), new Location(notebook.apiNotebook.cellAt(0).document.uri, new Position(2, 12)), false); // clamped - }); - - test('location, position mapping, cell-document changes', function () { - - let doc = new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook.apiNotebook, undefined); - - // UPDATE 1 - extHostNotebookDocuments.$acceptModelChanged(notebookUri, new SerializableObjectWithBuffers({ - versionId: notebook.apiNotebook.version + 1, - rawEvents: [ - { - - kind: NotebookCellsChangeType.ModelChange, - changes: [[0, 0, [{ - handle: 1, - uri: CellUri.generate(notebook.uri, 1), - source: ['Hello', 'World', 'Hello World!'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }, { - handle: 2, - uri: CellUri.generate(notebook.uri, 2), - source: ['Hallo', 'Welt', 'Hallo Welt!'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }]]] - } - ] - }), false); - assert.strictEqual(notebook.apiNotebook.cellCount, 1 + 2); - assert.strictEqual(doc.version, 1); - - assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); - assertLocation(doc, new Position(0, 0), new Location(notebook.apiNotebook.cellAt(0).document.uri, new Position(0, 0))); - assertLocation(doc, new Position(2, 2), new Location(notebook.apiNotebook.cellAt(0).document.uri, new Position(2, 2))); - assertLocation(doc, new Position(2, 12), new Location(notebook.apiNotebook.cellAt(0).document.uri, new Position(2, 12))); - assertLocation(doc, new Position(4, 0), new Location(notebook.apiNotebook.cellAt(1).document.uri, new Position(1, 0))); - assertLocation(doc, new Position(4, 3), new Location(notebook.apiNotebook.cellAt(1).document.uri, new Position(1, 3))); - - // offset math - let cell1End = doc.offsetAt(new Position(2, 12)); - assert.strictEqual(doc.positionAt(cell1End).isEqual(new Position(2, 12)), true); - - extHostDocuments.$acceptModelChanged(notebook.apiNotebook.cellAt(0).document.uri, { - versionId: 0, - eol: '\n', - changes: [{ - range: { startLineNumber: 3, startColumn: 1, endLineNumber: 3, endColumn: 6 }, - rangeLength: 6, - rangeOffset: 12, - text: 'Hi' - }], - isRedoing: false, - isUndoing: false, - }, false); - assertLines(doc, 'Hello', 'World', 'Hi World!', 'Hallo', 'Welt', 'Hallo Welt!'); - assertLocation(doc, new Position(2, 12), new Location(notebook.apiNotebook.cellAt(0).document.uri, new Position(2, 9)), false); - - assert.strictEqual(doc.positionAt(cell1End).isEqual(new Position(3, 2)), true); - - }); - - test('selector', function () { - - extHostNotebookDocuments.$acceptModelChanged(notebookUri, new SerializableObjectWithBuffers({ - versionId: notebook.apiNotebook.version + 1, - rawEvents: [ - { - kind: NotebookCellsChangeType.ModelChange, - changes: [[0, 0, [{ - handle: 1, - uri: CellUri.generate(notebook.uri, 1), - source: ['fooLang-document'], - eol: '\n', - language: 'fooLang', - cellKind: CellKind.Code, - outputs: [], - }, { - handle: 2, - uri: CellUri.generate(notebook.uri, 2), - source: ['barLang-document'], - eol: '\n', - language: 'barLang', - cellKind: CellKind.Code, - outputs: [], - }]]] - } - ] - }), false); - - const mixedDoc = new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook.apiNotebook, undefined); - const fooLangDoc = new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook.apiNotebook, 'fooLang'); - const barLangDoc = new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook.apiNotebook, 'barLang'); - - assertLines(mixedDoc, 'fooLang-document', 'barLang-document'); - assertLines(fooLangDoc, 'fooLang-document'); - assertLines(barLangDoc, 'barLang-document'); - - extHostNotebookDocuments.$acceptModelChanged(notebookUri, new SerializableObjectWithBuffers({ - versionId: notebook.apiNotebook.version + 1, - rawEvents: [ - { - kind: NotebookCellsChangeType.ModelChange, - changes: [[2, 0, [{ - handle: 3, - uri: CellUri.generate(notebook.uri, 3), - source: ['barLang-document2'], - eol: '\n', - language: 'barLang', - cellKind: CellKind.Code, - outputs: [], - }]]] - } - ] - }), false); - - assertLines(mixedDoc, 'fooLang-document', 'barLang-document', 'barLang-document2'); - assertLines(fooLangDoc, 'fooLang-document'); - assertLines(barLangDoc, 'barLang-document', 'barLang-document2'); - }); - - function assertOffsetAtPosition(doc: vscode.NotebookConcatTextDocument, offset: number, expected: { line: number; character: number }, reverse = true) { - const actual = doc.positionAt(offset); - - assert.strictEqual(actual.line, expected.line); - assert.strictEqual(actual.character, expected.character); - - if (reverse) { - const actualOffset = doc.offsetAt(actual); - assert.strictEqual(actualOffset, offset); - } - } - - - test('offsetAt(position) <-> positionAt(offset)', function () { - - extHostNotebookDocuments.$acceptModelChanged(notebookUri, new SerializableObjectWithBuffers({ - versionId: notebook.apiNotebook.version + 1, - rawEvents: [ - { - kind: NotebookCellsChangeType.ModelChange, - changes: [[0, 0, [{ - handle: 1, - uri: CellUri.generate(notebook.uri, 1), - source: ['Hello', 'World', 'Hello World!'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }, { - handle: 2, - uri: CellUri.generate(notebook.uri, 2), - source: ['Hallo', 'Welt', 'Hallo Welt!'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }]]] - } - ] - }), false); - - assert.strictEqual(notebook.apiNotebook.cellCount, 1 + 2); // markdown and code - - let doc = new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook.apiNotebook, undefined); - assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); - - assertOffsetAtPosition(doc, 0, { line: 0, character: 0 }); - assertOffsetAtPosition(doc, 1, { line: 0, character: 1 }); - assertOffsetAtPosition(doc, 9, { line: 1, character: 3 }); - assertOffsetAtPosition(doc, 32, { line: 4, character: 1 }); - assertOffsetAtPosition(doc, 47, { line: 5, character: 11 }); - }); - - - function assertLocationAtPosition(doc: vscode.NotebookConcatTextDocument, pos: { line: number; character: number }, expected: { uri: URI; line: number; character: number }, reverse = true) { - - const actual = doc.locationAt(new Position(pos.line, pos.character)); - assert.strictEqual(actual.uri.toString(), expected.uri.toString()); - assert.strictEqual(actual.range.start.line, expected.line); - assert.strictEqual(actual.range.end.line, expected.line); - assert.strictEqual(actual.range.start.character, expected.character); - assert.strictEqual(actual.range.end.character, expected.character); - - if (reverse) { - const actualPos = doc.positionAt(actual); - assert.strictEqual(actualPos.line, pos.line); - assert.strictEqual(actualPos.character, pos.character); - } - } - - test('locationAt(position) <-> positionAt(location)', function () { - - extHostNotebookDocuments.$acceptModelChanged(notebookUri, new SerializableObjectWithBuffers({ - versionId: notebook.apiNotebook.version + 1, - rawEvents: [ - { - kind: NotebookCellsChangeType.ModelChange, - changes: [[0, 0, [{ - handle: 1, - uri: CellUri.generate(notebook.uri, 1), - source: ['Hello', 'World', 'Hello World!'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }, { - handle: 2, - uri: CellUri.generate(notebook.uri, 2), - source: ['Hallo', 'Welt', 'Hallo Welt!'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }]]] - } - ] - }), false); - - assert.strictEqual(notebook.apiNotebook.cellCount, 1 + 2); // markdown and code - - let doc = new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook.apiNotebook, undefined); - assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); - - assertLocationAtPosition(doc, { line: 0, character: 0 }, { uri: notebook.apiNotebook.cellAt(0).document.uri, line: 0, character: 0 }); - assertLocationAtPosition(doc, { line: 2, character: 0 }, { uri: notebook.apiNotebook.cellAt(0).document.uri, line: 2, character: 0 }); - assertLocationAtPosition(doc, { line: 2, character: 12 }, { uri: notebook.apiNotebook.cellAt(0).document.uri, line: 2, character: 12 }); - assertLocationAtPosition(doc, { line: 3, character: 0 }, { uri: notebook.apiNotebook.cellAt(1).document.uri, line: 0, character: 0 }); - assertLocationAtPosition(doc, { line: 5, character: 0 }, { uri: notebook.apiNotebook.cellAt(1).document.uri, line: 2, character: 0 }); - assertLocationAtPosition(doc, { line: 5, character: 11 }, { uri: notebook.apiNotebook.cellAt(1).document.uri, line: 2, character: 11 }); - }); - - test('getText(range)', function () { - - extHostNotebookDocuments.$acceptModelChanged(notebookUri, new SerializableObjectWithBuffers({ - versionId: notebook.apiNotebook.version + 1, - rawEvents: [ - { - kind: NotebookCellsChangeType.ModelChange, - changes: [[0, 0, [{ - handle: 1, - uri: CellUri.generate(notebook.uri, 1), - source: ['Hello', 'World', 'Hello World!'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }, { - handle: 2, - uri: CellUri.generate(notebook.uri, 2), - source: ['Hallo', 'Welt', 'Hallo Welt!'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }, { - handle: 3, - uri: CellUri.generate(notebook.uri, 3), - source: ['Three', 'Drei', 'Drüü'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }]]] - } - ] - }), false); - - assert.strictEqual(notebook.apiNotebook.cellCount, 1 + 3); // markdown and code - - let doc = new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook.apiNotebook, undefined); - assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!', 'Three', 'Drei', 'Drüü'); - - assert.strictEqual(doc.getText(new Range(0, 0, 0, 0)), ''); - assert.strictEqual(doc.getText(new Range(0, 0, 1, 0)), 'Hello\n'); - assert.strictEqual(doc.getText(new Range(2, 0, 4, 0)), 'Hello World!\nHallo\n'); - assert.strictEqual(doc.getText(new Range(2, 0, 8, 0)), 'Hello World!\nHallo\nWelt\nHallo Welt!\nThree\nDrei\n'); - }); - - test('validateRange/Position', function () { - - extHostNotebookDocuments.$acceptModelChanged(notebookUri, new SerializableObjectWithBuffers({ - versionId: notebook.apiNotebook.version + 1, - rawEvents: [ - { - kind: NotebookCellsChangeType.ModelChange, - changes: [[0, 0, [{ - handle: 1, - uri: CellUri.generate(notebook.uri, 1), - source: ['Hello', 'World', 'Hello World!'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }, { - handle: 2, - uri: CellUri.generate(notebook.uri, 2), - source: ['Hallo', 'Welt', 'Hallo Welt!'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }]]] - } - ] - }), false); - - assert.strictEqual(notebook.apiNotebook.cellCount, 1 + 2); // markdown and code - - let doc = new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook.apiNotebook, undefined); - assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); - - - function assertPosition(actual: vscode.Position, expectedLine: number, expectedCh: number) { - assert.strictEqual(actual.line, expectedLine); - assert.strictEqual(actual.character, expectedCh); - } - - - // "fixed" - assertPosition(doc.validatePosition(new Position(0, 1000)), 0, 5); - assertPosition(doc.validatePosition(new Position(2, 1000)), 2, 12); - assertPosition(doc.validatePosition(new Position(5, 1000)), 5, 11); - assertPosition(doc.validatePosition(new Position(5000, 1000)), 5, 11); - - // "good" - assertPosition(doc.validatePosition(new Position(0, 1)), 0, 1); - assertPosition(doc.validatePosition(new Position(0, 5)), 0, 5); - assertPosition(doc.validatePosition(new Position(2, 8)), 2, 8); - assertPosition(doc.validatePosition(new Position(2, 12)), 2, 12); - assertPosition(doc.validatePosition(new Position(5, 11)), 5, 11); - - }); -}); diff --git a/src/vs/workbench/api/test/browser/extHostTypeConverter.test.ts b/src/vs/workbench/api/test/browser/extHostTypeConverter.test.ts index 20ef8c11b6c..fb2b02bc890 100644 --- a/src/vs/workbench/api/test/browser/extHostTypeConverter.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTypeConverter.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; -import { MarkdownString, NotebookCellOutputItem, NotebookData } from 'vs/workbench/api/common/extHostTypeConverters'; +import { MarkdownString, NotebookCellOutputItem, NotebookData, LanguageSelector } from 'vs/workbench/api/common/extHostTypeConverters'; import { isEmptyObject } from 'vs/base/common/types'; import { forEach } from 'vs/base/common/collections'; import { LogLevel as _MainLogLevel } from 'vs/platform/log/common/log'; @@ -111,4 +111,16 @@ suite('ExtHostTypeConverter', function () { assert.strictEqual(item2.mime, item.mime); assert.deepStrictEqual(Array.from(item2.data), Array.from(item.data)); }); + + test('LanguageSelector', function () { + const out = LanguageSelector.from({ language: 'bat', notebookType: 'xxx' }); + assert.ok(typeof out === 'object'); + assert.deepStrictEqual(out, { + language: 'bat', + notebookType: 'xxx', + scheme: undefined, + pattern: undefined, + exclusive: undefined, + }); + }); }); diff --git a/src/vs/workbench/api/test/browser/extHostWebview.test.ts b/src/vs/workbench/api/test/browser/extHostWebview.test.ts index cce4f74d246..50327ab707d 100644 --- a/src/vs/workbench/api/test/browser/extHostWebview.test.ts +++ b/src/vs/workbench/api/test/browser/extHostWebview.test.ts @@ -55,7 +55,8 @@ suite('ExtHostWebview', () => { title: 'title', state: {}, panelOptions: {}, - webviewOptions: {} + webviewOptions: {}, + active: true, }, 0 as EditorGroupColumn); assert.strictEqual(lastInvokedDeserializer, serializerA); @@ -71,7 +72,8 @@ suite('ExtHostWebview', () => { title: 'title', state: {}, panelOptions: {}, - webviewOptions: {} + webviewOptions: {}, + active: true, }, 0 as EditorGroupColumn); assert.strictEqual(lastInvokedDeserializer, serializerB); }); diff --git a/src/vs/workbench/api/test/browser/extHostWorkspace.test.ts b/src/vs/workbench/api/test/browser/extHostWorkspace.test.ts index 5c05a247db3..c5fa028c83c 100644 --- a/src/vs/workbench/api/test/browser/extHostWorkspace.test.ts +++ b/src/vs/workbench/api/test/browser/extHostWorkspace.test.ts @@ -290,6 +290,7 @@ suite('ExtHostWorkspace', function () { const protocol: IMainContext = { getProxy: () => { return undefined!; }, set: () => { return undefined!; }, + dispose: () => { }, assertRegistered: () => { }, drain: () => { return undefined!; }, }; diff --git a/src/vs/workbench/api/test/browser/mainThreadDiagnostics.test.ts b/src/vs/workbench/api/test/browser/mainThreadDiagnostics.test.ts index e30482a8eac..de58c41de23 100644 --- a/src/vs/workbench/api/test/browser/mainThreadDiagnostics.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadDiagnostics.test.ts @@ -30,6 +30,7 @@ suite('MainThreadDiagnostics', function () { new class implements IExtHostContext { remoteAuthority = ''; extensionHostKind = ExtensionHostKind.LocalProcess; + dispose() { } assertRegistered() { } set(v: any): any { return null; } getProxy(): any { @@ -71,6 +72,7 @@ suite('MainThreadDiagnostics', function () { new class implements IExtHostContext { remoteAuthority = ''; extensionHostKind = ExtensionHostKind.LocalProcess; + dispose() { } assertRegistered() { } set(v: any): any { return null; } getProxy(): any { @@ -134,6 +136,7 @@ suite('MainThreadDiagnostics', function () { new class implements IExtHostContext { remoteAuthority = ''; extensionHostKind = ExtensionHostKind.LocalProcess; + dispose() { } assertRegistered() { } set(v: any): any { return null; } getProxy(): any { diff --git a/src/vs/workbench/api/test/browser/mainThreadTreeViews.test.ts b/src/vs/workbench/api/test/browser/mainThreadTreeViews.test.ts index 0803d8aabba..1386a01b234 100644 --- a/src/vs/workbench/api/test/browser/mainThreadTreeViews.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadTreeViews.test.ts @@ -67,6 +67,7 @@ suite('MainThreadHostTreeView', function () { new class implements IExtHostContext { remoteAuthority = ''; extensionHostKind = ExtensionHostKind.LocalProcess; + dispose() { } assertRegistered() { } set(v: any): any { return null; } getProxy(): any { diff --git a/src/vs/workbench/api/test/common/testRPCProtocol.ts b/src/vs/workbench/api/test/common/testRPCProtocol.ts index 52a307debc9..2947d378288 100644 --- a/src/vs/workbench/api/test/common/testRPCProtocol.ts +++ b/src/vs/workbench/api/test/common/testRPCProtocol.ts @@ -21,6 +21,7 @@ export function SingleProxyRPCProtocol(thing: any): IExtHostContext & IExtHostRp set<T, R extends T>(identifier: ProxyIdentifier<T>, value: R): R { return value; }, + dispose: undefined!, assertRegistered: undefined!, drain: undefined!, extensionHostKind: ExtensionHostKind.LocalProcess @@ -135,6 +136,10 @@ export class TestRPCProtocol implements IExtHostContext, IExtHostRpcService { }); } + public dispose() { + throw new Error('Not implemented!'); + } + public assertRegistered(identifiers: ProxyIdentifier<any>[]): void { throw new Error('Not implemented!'); } diff --git a/src/vs/workbench/api/worker/extHostExtensionService.ts b/src/vs/workbench/api/worker/extHostExtensionService.ts index 68c7c834910..a74fcfbc69b 100644 --- a/src/vs/workbench/api/worker/extHostExtensionService.ts +++ b/src/vs/workbench/api/worker/extHostExtensionService.ts @@ -44,7 +44,7 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { // initialize API and register actors const apiFactory = this._instaService.invokeFunction(createApiFactoryAndRegisterActors); - this._fakeModules = this._instaService.createInstance(WorkerRequireInterceptor, apiFactory, this._registry); + this._fakeModules = this._instaService.createInstance(WorkerRequireInterceptor, apiFactory, { mine: this._myRegistry, all: this._globalRegistry }); await this._fakeModules.install(); performance.mark('code/extHost/didInitAPI'); diff --git a/src/vs/workbench/browser/actions/layoutActions.ts b/src/vs/workbench/browser/actions/layoutActions.ts index aaf84ac8ef7..8440c0f8e12 100644 --- a/src/vs/workbench/browser/actions/layoutActions.ts +++ b/src/vs/workbench/browser/actions/layoutActions.ts @@ -22,7 +22,7 @@ import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/b import { ToggleAuxiliaryBarAction } from 'vs/workbench/browser/parts/auxiliarybar/auxiliaryBarActions'; import { TogglePanelAction } from 'vs/workbench/browser/parts/panel/panelActions'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { AuxiliaryBarVisibleContext, PanelAlignmentContext, PanelVisibleContext, SideBarVisibleContext, FocusedViewContext, InEditorZenModeContext, IsCenteredLayoutContext, EditorAreaVisibleContext, IsFullscreenContext } from 'vs/workbench/common/contextkeys'; +import { AuxiliaryBarVisibleContext, PanelAlignmentContext, PanelVisibleContext, SideBarVisibleContext, FocusedViewContext, InEditorZenModeContext, IsCenteredLayoutContext, EditorAreaVisibleContext, IsFullscreenContext, PanelPositionContext } from 'vs/workbench/common/contextkeys'; import { Codicon } from 'vs/base/common/codicons'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; @@ -330,12 +330,8 @@ registerAction2(class extends Action2 { category: CATEGORIES.View, f1: true, toggled: EditorAreaVisibleContext, - // Remove from appearance menu - // menu: [{ - // id: MenuId.MenubarAppearanceMenu, - // group: '2_workbench_layout', - // order: 5 - // }] + // the workbench grid currently prevents us from supporting panel maximization with non-center panel alignment + precondition: ContextKeyExpr.or(PanelAlignmentContext.isEqualTo('center'), PanelPositionContext.notEqualsTo('bottom')) }); } @@ -505,13 +501,7 @@ registerAction2(class extends Action2 { original: 'Toggle Tab Visibility' }, category: CATEGORIES.View, - f1: true, - keybinding: { - weight: KeybindingWeight.WorkbenchContrib, - primary: undefined, - mac: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KeyW, }, - linux: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KeyW, } - } + f1: true }); } diff --git a/src/vs/workbench/browser/actions/quickAccessActions.ts b/src/vs/workbench/browser/actions/quickAccessActions.ts index f587eb23bf8..f3f41c78e1d 100644 --- a/src/vs/workbench/browser/actions/quickAccessActions.ts +++ b/src/vs/workbench/browser/actions/quickAccessActions.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { MenuRegistry, MenuId, Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { MenuId, Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { KeybindingsRegistry, KeybindingWeight, IKeybindingRule } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IQuickInputService, ItemActivation } from 'vs/platform/quickinput/common/quickInput'; @@ -22,21 +22,6 @@ const globalQuickAccessKeybinding = { mac: { primary: KeyMod.CtrlCmd | KeyCode.KeyP, secondary: undefined } }; -const QUICKACCESS_ACTION_ID = 'workbench.action.quickOpen'; - -MenuRegistry.appendMenuItem(MenuId.CommandPalette, { - command: { id: QUICKACCESS_ACTION_ID, title: { value: localize('quickOpen', "Go to File..."), original: 'Go to File...' } } -}); - -KeybindingsRegistry.registerKeybindingRule({ - id: QUICKACCESS_ACTION_ID, - weight: KeybindingWeight.WorkbenchContrib, - when: undefined, - primary: globalQuickAccessKeybinding.primary, - secondary: globalQuickAccessKeybinding.secondary, - mac: globalQuickAccessKeybinding.mac -}); - KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'workbench.action.closeQuickOpen', weight: KeybindingWeight.WorkbenchContrib, @@ -131,21 +116,41 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } }); -CommandsRegistry.registerCommand({ - id: QUICKACCESS_ACTION_ID, - handler: async function (accessor: ServicesAccessor, prefix: unknown) { - const quickInputService = accessor.get(IQuickInputService); +registerAction2(class QuickAccessAction extends Action2 { + constructor() { + super({ + id: 'workbench.action.quickOpen', + title: { + value: localize('quickOpen', "Go to File..."), + original: 'Go to File...' + }, + description: { + description: `Quick access`, + args: [{ + name: 'prefix', + schema: { + 'type': 'string' + } + }] + }, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: globalQuickAccessKeybinding.primary, + secondary: globalQuickAccessKeybinding.secondary, + mac: globalQuickAccessKeybinding.mac + }, + f1: true, + menu: { + id: MenuId.TitleMenuQuickPick, + group: '1/workspaceNav', + order: 1 + } + }); + } + run(accessor: ServicesAccessor, prefix: undefined): void { + const quickInputService = accessor.get(IQuickInputService); quickInputService.quickAccess.show(typeof prefix === 'string' ? prefix : undefined, { preserveValue: typeof prefix === 'string' /* preserve as is if provided */ }); - }, - description: { - description: `Quick access`, - args: [{ - name: 'prefix', - schema: { - 'type': 'string' - } - }] } }); diff --git a/src/vs/workbench/browser/actions/windowActions.ts b/src/vs/workbench/browser/actions/windowActions.ts index 748ababc6db..a0d14adf991 100644 --- a/src/vs/workbench/browser/actions/windowActions.ts +++ b/src/vs/workbench/browser/actions/windowActions.ts @@ -145,6 +145,7 @@ abstract class BaseOpenRecentAction extends Action2 { matchOnDescription: true, onKeyMods: mods => keyMods = mods, quickNavigate: this.isQuickNavigate() ? { keybindings: keybindingService.lookupKeybindings(this.desc.id) } : undefined, + hideInput: this.isQuickNavigate(), onDidTriggerItemButton: async context => { // Remove diff --git a/src/vs/workbench/browser/labels.ts b/src/vs/workbench/browser/labels.ts index e0b778dd82f..4cfe9f32bb5 100644 --- a/src/vs/workbench/browser/labels.ts +++ b/src/vs/workbench/browser/labels.ts @@ -272,14 +272,15 @@ class ResourceLabelWidget extends IconLabel { private readonly _onDidRender = this._register(new Emitter<void>()); readonly onDidRender = this._onDidRender.event; - private label?: IResourceLabelProps; + private label: IResourceLabelProps | undefined = undefined; private decoration = this._register(new MutableDisposable<IDecoration>()); - private options?: IResourceLabelOptions; - private computedIconClasses?: string[]; - private lastKnownDetectedLanguageId?: string; - private computedPathLabel?: string; + private options: IResourceLabelOptions | undefined = undefined; - private needsRedraw?: Redraw; + private computedIconClasses: string[] | undefined = undefined; + private computedLanguageId: string | undefined = undefined; + private computedPathLabel: string | undefined = undefined; + + private needsRedraw: Redraw | undefined = undefined; private isHidden: boolean = false; constructor( @@ -325,8 +326,8 @@ class ResourceLabelWidget extends IconLabel { } if (isEqual(model.uri, resource)) { - if (this.lastKnownDetectedLanguageId !== model.getLanguageId()) { - this.lastKnownDetectedLanguageId = model.getLanguageId(); + if (this.computedLanguageId !== model.getLanguageId()) { + this.computedLanguageId = model.getLanguageId(); this.render({ updateIcon: true, updateDecoration: false }); // update if the language id of the model has changed from our last known state } } @@ -444,8 +445,12 @@ class ResourceLabelWidget extends IconLabel { this.label = label; this.options = options; + if (hasResourceChanged) { + this.computedLanguageId = undefined; // reset computed language since resource changed + } + if (hasPathLabelChanged) { - this.computedPathLabel = undefined; // reset path label due to resource change + this.computedPathLabel = undefined; // reset path label due to resource/path-label change } this.render({ @@ -485,7 +490,7 @@ class ResourceLabelWidget extends IconLabel { clear(): void { this.label = undefined; this.options = undefined; - this.lastKnownDetectedLanguageId = undefined; + this.computedLanguageId = undefined; this.computedIconClasses = undefined; this.computedPathLabel = undefined; @@ -594,7 +599,7 @@ class ResourceLabelWidget extends IconLabel { this.label = undefined; this.options = undefined; - this.lastKnownDetectedLanguageId = undefined; + this.computedLanguageId = undefined; this.computedIconClasses = undefined; this.computedPathLabel = undefined; } diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 1bf6c4750e2..88e4d4f892d 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -617,16 +617,22 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi return { filesToOpenOrCreate: defaultLayout.editors.map<IPath>(file => { + const legacyOverride = file.openWith; + const legacySelection = file.selection && file.selection.start && isNumber(file.selection.start.line) ? { + startLineNumber: file.selection.start.line, + startColumn: isNumber(file.selection.start.column) ? file.selection.start.column : 1, + endLineNumber: isNumber(file.selection.end.line) ? file.selection.end.line : undefined, + endColumn: isNumber(file.selection.end.line) ? (isNumber(file.selection.end.column) ? file.selection.end.column : 1) : undefined, + } : undefined; + return { fileUri: URI.revive(file.uri), - selection: file.selection && file.selection.start && isNumber(file.selection.start.line) ? { - startLineNumber: file.selection.start.line, - startColumn: isNumber(file.selection.start.column) ? file.selection.start.column : 1, - endLineNumber: isNumber(file.selection.end.line) ? file.selection.end.line : undefined, - endColumn: isNumber(file.selection.end.line) ? (isNumber(file.selection.end.column) ? file.selection.end.column : 1) : undefined, - } : undefined, openOnlyIfExists: file.openOnlyIfExists, - editorOverrideId: file.openWith + options: { + selection: legacySelection, + override: legacyOverride, + ...file.options // keep at the end to override legacy selection/override that may be `undefined` + } }; }) }; @@ -2109,13 +2115,15 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi }; type StartupLayoutEventClassification = { - activityBarVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; - sideBarVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; - auxiliaryBarVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; - panelVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; - statusbarVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; - sideBarPosition: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; - panelPosition: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; + owner: 'sbatten'; + comment: 'Information about the layout of the workbench during statup'; + activityBarVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether or the not the activity bar is visible' }; + sideBarVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether or the not the primary side bar is visible' }; + auxiliaryBarVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether or the not the secondary side bar is visible' }; + panelVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether or the not the panel is visible' }; + statusbarVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether or the not the status bar is visible' }; + sideBarPosition: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the primary side bar is on the left or right' }; + panelPosition: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the panel is on the bottom, left, or right' }; }; const layoutDescriptor: StartupLayoutEvent = { diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index 69aad292e8a..c94c38b2e47 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -42,6 +42,8 @@ import { StringSHA1 } from 'vs/base/common/hash'; import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; import { GestureEvent } from 'vs/base/browser/touch'; import { IPaneCompositePart, IPaneCompositeSelectorPart } from 'vs/workbench/browser/parts/paneCompositePart'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { Extensions, IProfileStorageRegistry } from 'vs/workbench/services/profiles/common/profileStorageRegistry'; interface IPlaceholderViewContainer { readonly id: string; @@ -140,6 +142,15 @@ export class ActivitybarPart extends Part implements IPaneCompositeSelectorPart this.onDidRegisterViewContainers(this.getViewContainers()); this.registerListeners(); + + Registry.as<IProfileStorageRegistry>(Extensions.ProfileStorageRegistry) + .registerKeys([{ + key: ActivitybarPart.PINNED_VIEW_CONTAINERS, + description: localize('pinned view containers', "Activity bar entries visibility customizations") + }, { + key: AccountsActivityActionViewItem.ACCOUNTS_VISIBILITY_PREFERENCE_KEY, + description: localize('accounts visibility key', "Accounts entry visibility customization in the activity bar.") + }]); } private createCompositeBar() { diff --git a/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css b/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css index 169ba8a11f0..abe1427ca20 100644 --- a/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css +++ b/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css @@ -37,14 +37,6 @@ justify-content: space-between; } -/* - * Fix menu jumping and background inheritance in Safari - */ -.monaco-workbench.safari .activitybar > .content { - position: absolute; - background-color: inherit; -} - /** Viewlet Switcher */ .monaco-workbench .activitybar > .content .monaco-action-bar { diff --git a/src/vs/workbench/browser/parts/banner/bannerPart.ts b/src/vs/workbench/browser/parts/banner/bannerPart.ts index b28390d2680..7750ccf5fe3 100644 --- a/src/vs/workbench/browser/parts/banner/bannerPart.ts +++ b/src/vs/workbench/browser/parts/banner/bannerPart.ts @@ -43,7 +43,8 @@ registerThemingParticipant((theme, collector) => { collector.addRule(` .monaco-workbench .part.banner, .monaco-workbench .part.banner .action-container .codicon, - .monaco-workbench .part.banner .message-actions-container .monaco-link + .monaco-workbench .part.banner .message-actions-container .monaco-link, + .monaco-workbench .part.banner .message-container a { color: ${foregroundColor}; } `); } @@ -240,9 +241,8 @@ export class BannerPart extends Part implements IBannerService { messageContainer.appendChild(this.getBannerMessage(item.message)); // Message Actions + this.messageActionsContainer = append(this.element, $('div.message-actions-container')); if (item.actions) { - this.messageActionsContainer = append(this.element, $('div.message-actions-container')); - for (const action of item.actions) { this._register(this.instantiationService.createInstance(Link, this.messageActionsContainer, { ...action, tabIndex: -1 }, {})); } diff --git a/src/vs/workbench/browser/parts/banner/media/bannerpart.css b/src/vs/workbench/browser/parts/banner/media/bannerpart.css index 94d535fb462..fb2d5b4d1e8 100644 --- a/src/vs/workbench/browser/parts/banner/media/bannerpart.css +++ b/src/vs/workbench/browser/parts/banner/media/bannerpart.css @@ -54,6 +54,10 @@ text-decoration: underline; } +.monaco-workbench .part.banner .message-container a { + text-decoration: underline; +} + .monaco-workbench .part.banner .action-container { padding: 0 10px 0 6px; } diff --git a/src/vs/workbench/browser/parts/compositeBar.ts b/src/vs/workbench/browser/parts/compositeBar.ts index 68f6ebacaa8..de6e99067ef 100644 --- a/src/vs/workbench/browser/parts/compositeBar.ts +++ b/src/vs/workbench/browser/parts/compositeBar.ts @@ -227,6 +227,7 @@ export class CompositeBar extends Widget implements ICompositeBar { }, orientation: this.options.orientation, ariaLabel: localize('activityBarAriaLabel', "Active View Switcher"), + ariaRole: 'tablist', animated: false, preventLoopNavigation: this.options.preventLoopNavigation, triggerKeys: { keyDown: true } @@ -426,12 +427,6 @@ export class CompositeBar extends Widget implements ICompositeBar { this.options.openComposite(defaultCompositeId, true); } - // Case: we closed the last visible composite - // Solv: we hide the part - else if (this.visibleComposites.length <= 1) { - this.options.hidePart(); - } - // Case: we closed the default composite // Solv: we open the next visible composite from top else { diff --git a/src/vs/workbench/browser/parts/compositeBarActions.ts b/src/vs/workbench/browser/parts/compositeBarActions.ts index 3bfc0a6eca2..df8b6bc78d0 100644 --- a/src/vs/workbench/browser/parts/compositeBarActions.ts +++ b/src/vs/workbench/browser/parts/compositeBarActions.ts @@ -7,7 +7,7 @@ import { localize } from 'vs/nls'; import { Action, IAction, Separator } from 'vs/base/common/actions'; import { $, addDisposableListener, append, clearNode, EventHelper, EventType, getDomNodePagePosition, hide, show } from 'vs/base/browser/dom'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { dispose, toDisposable, MutableDisposable, IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { dispose, toDisposable, MutableDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IThemeService, IColorTheme, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { TextBadge, NumberBadge, IBadge, IconBadge, ProgressBadge } from 'vs/workbench/services/activity/common/activity'; @@ -21,7 +21,7 @@ import { CompositeDragAndDropObserver, ICompositeDragAndDrop, Before2D, toggleDr import { Color } from 'vs/base/common/color'; import { IBaseActionViewItemOptions, BaseActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { Codicon } from 'vs/base/common/codicons'; -import { IHoverService } from 'vs/workbench/services/hover/browser/hover'; +import { IHoverService, IHoverWidget } from 'vs/workbench/services/hover/browser/hover'; import { RunOnceScheduler } from 'vs/base/common/async'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; @@ -149,7 +149,7 @@ export class ActivityActionViewItem extends BaseActionViewItem { private keybindingLabel: string | undefined | null; private readonly hoverDisposables = this._register(new DisposableStore()); - private readonly hover = this._register(new MutableDisposable<IDisposable>()); + private lastHover: IHoverWidget | undefined; private readonly showHoverScheduler = new RunOnceScheduler(() => this.showHover(), 0); private static _hoverLeaveTime = 0; @@ -397,21 +397,21 @@ export class ActivityActionViewItem extends BaseActionViewItem { }, true)); this.hoverDisposables.add(addDisposableListener(this.container, EventType.MOUSE_LEAVE, () => { ActivityActionViewItem._hoverLeaveTime = Date.now(); - this.hover.value = undefined; + this.hoverService.hideHover(); this.showHoverScheduler.cancel(); }, true)); this.hoverDisposables.add(toDisposable(() => { - this.hover.value = undefined; + this.hoverService.hideHover(); this.showHoverScheduler.cancel(); })); } private showHover(skipFadeInAnimation: boolean = false): void { - if (this.hover.value) { + if (this.lastHover && !this.lastHover.isDisposed) { return; } const hoverPosition = this.options.hoverOptions!.position(); - this.hover.value = this.hoverService.showHover({ + this.lastHover = this.hoverService.showHover({ target: this.container, hoverPosition, content: this.computeTitle(), diff --git a/src/vs/workbench/browser/parts/editor/binaryEditor.ts b/src/vs/workbench/browser/parts/editor/binaryEditor.ts index 99701724a23..4f54228962d 100644 --- a/src/vs/workbench/browser/parts/editor/binaryEditor.ts +++ b/src/vs/workbench/browser/parts/editor/binaryEditor.ts @@ -3,26 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./media/binaryeditor'; import { localize } from 'vs/nls'; import { Emitter } from 'vs/base/common/event'; -import { IEditorOpenContext } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; -import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; -import { ScrollbarVisibility } from 'vs/base/common/scrollable'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { Dimension, size, clearNode } from 'vs/base/browser/dom'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { IStorageService } from 'vs/platform/storage/common/storage'; -import { assertIsDefined, assertAllDefined } from 'vs/base/common/types'; import { ByteSize } from 'vs/platform/files/common/files'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { Link } from 'vs/platform/opener/browser/link'; +import { EditorPlaceholder, IEditorPlaceholderContents } from 'vs/workbench/browser/parts/editor/editorPlaceholder'; export interface IOpenCallbacks { openInternal: (input: EditorInput, options: IEditorOptions | undefined) => Promise<void>; @@ -31,7 +22,7 @@ export interface IOpenCallbacks { /* * This class is only intended to be subclassed and not instantiated. */ -export abstract class BaseBinaryResourceEditor extends EditorPane { +export abstract class BaseBinaryResourceEditor extends EditorPlaceholder { private readonly _onDidChangeMetadata = this._register(new Emitter<void>()); readonly onDidChangeMetadata = this._onDidChangeMetadata.event; @@ -40,9 +31,6 @@ export abstract class BaseBinaryResourceEditor extends EditorPane { readonly onDidOpenInPlace = this._onDidOpenInPlace.event; private metadata: string | undefined; - private binaryContainer: HTMLElement | undefined; - private scrollbar: DomScrollableElement | undefined; - private inputDisposable = this._register(new MutableDisposable()); constructor( id: string, @@ -50,78 +38,44 @@ export abstract class BaseBinaryResourceEditor extends EditorPane { telemetryService: ITelemetryService, themeService: IThemeService, @IStorageService storageService: IStorageService, - @IInstantiationService private readonly instantiationService: IInstantiationService + @IInstantiationService instantiationService: IInstantiationService ) { - super(id, telemetryService, themeService, storageService); + super(id, telemetryService, themeService, storageService, instantiationService); } override getTitle(): string { return this.input ? this.input.getName() : localize('binaryEditor', "Binary Viewer"); } - protected createEditor(parent: HTMLElement): void { - - // Container for Binary - this.binaryContainer = document.createElement('div'); - this.binaryContainer.className = 'monaco-binary-resource-editor'; - this.binaryContainer.style.outline = 'none'; - this.binaryContainer.tabIndex = 0; // enable focus support from the editor part (do not remove) - - // Custom Scrollbars - this.scrollbar = this._register(new DomScrollableElement(this.binaryContainer, { horizontal: ScrollbarVisibility.Auto, vertical: ScrollbarVisibility.Auto })); - parent.appendChild(this.scrollbar.getDomNode()); - } - - override async setInput(input: EditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise<void> { - await super.setInput(input, options, context, token); + protected async getContents(input: EditorInput, options: IEditorOptions): Promise<IEditorPlaceholderContents> { const model = await input.resolve(); - // Check for cancellation - if (token.isCancellationRequested) { - return; - } - // Assert Model instance if (!(model instanceof BinaryEditorModel)) { throw new Error('Unable to open file as binary'); } - // Render Input - this.inputDisposable.value = this.renderInput(input, options, model); - } - - private renderInput(input: EditorInput, options: IEditorOptions | undefined, model: BinaryEditorModel): IDisposable { - const [binaryContainer, scrollbar] = assertAllDefined(this.binaryContainer, this.scrollbar); - - clearNode(binaryContainer); - - const disposables = new DisposableStore(); - - const label = document.createElement('p'); - label.textContent = localize('nativeBinaryError', "The file is not displayed in the editor because it is either binary or uses an unsupported text encoding."); - binaryContainer.appendChild(label); - - this._register(this.instantiationService.createInstance(Link, label, { - label: localize('openAsText', "Do you want to open it anyway?"), - href: '' - }, { - opener: async () => { - - // Open in place - await this.callbacks.openInternal(input, options); - - // Signal to listeners that the binary editor has been opened in-place - this._onDidOpenInPlace.fire(); - } - })); - - scrollbar.scanDomNode(); - // Update metadata const size = model.getSize(); this.handleMetadataChanged(typeof size === 'number' ? ByteSize.formatSize(size) : ''); - return disposables; + return { + icon: '$(warning)', + label: localize('binaryError', "The file is not displayed in the editor because it is either binary or uses an unsupported text encoding."), + actions: [ + { + label: localize('openAnyway', "Open Anyway"), + run: async () => { + + // Open in place + await this.callbacks.openInternal(input, options); + + // Signal to listeners that the binary editor has been opened in-place + this._onDidOpenInPlace.fire(); + } + } + ] + }; } private handleMetadataChanged(meta: string | undefined): void { @@ -133,38 +87,4 @@ export abstract class BaseBinaryResourceEditor extends EditorPane { getMetadata(): string | undefined { return this.metadata; } - - override clearInput(): void { - - // Clear Meta - this.handleMetadataChanged(undefined); - - // Clear the rest - if (this.binaryContainer) { - clearNode(this.binaryContainer); - } - this.inputDisposable.clear(); - - super.clearInput(); - } - - layout(dimension: Dimension): void { - - // Pass on to Binary Container - const [binaryContainer, scrollbar] = assertAllDefined(this.binaryContainer, this.scrollbar); - size(binaryContainer, dimension.width, dimension.height); - scrollbar.scanDomNode(); - } - - override focus(): void { - const binaryContainer = assertIsDefined(this.binaryContainer); - - binaryContainer.focus(); - } - - override dispose(): void { - this.binaryContainer?.remove(); - - super.dispose(); - } } diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts index 276c37b1bd0..a969831de5b 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts @@ -14,8 +14,7 @@ import { extUri } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import 'vs/css!./media/breadcrumbscontrol'; import { localize } from 'vs/nls'; -import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; @@ -494,26 +493,32 @@ export class BreadcrumbsControl { //#region commands // toggle command -MenuRegistry.appendMenuItem(MenuId.CommandPalette, { - command: { - id: 'breadcrumbs.toggle', - title: { value: localize('cmd.toggle', "Toggle Breadcrumbs"), original: 'Toggle Breadcrumbs' }, - category: CATEGORIES.View +registerAction2(class ToggleBreadcrumb extends Action2 { + + constructor() { + super({ + id: 'breadcrumbs.toggle', + title: { + value: localize('cmd.toggle', "Toggle Breadcrumbs"), + mnemonicTitle: localize('miShowBreadcrumbs', "Show &&Breadcrumbs"), + original: 'Toggle Breadcrumbs', + }, + category: CATEGORIES.View, + toggled: ContextKeyExpr.equals('config.breadcrumbs.enabled', true), + menu: [ + { id: MenuId.CommandPalette }, + { id: MenuId.MenubarViewMenu, group: '5_editor', order: 3 }, + { id: MenuId.NotebookToolbar, group: 'notebookLayout', order: 2 } + ] + }); } -}); -MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { - group: '5_editor', - order: 3, - command: { - id: 'breadcrumbs.toggle', - title: localize('miShowBreadcrumbs', "Show &&Breadcrumbs"), - toggled: ContextKeyExpr.equals('config.breadcrumbs.enabled', true) + + run(accessor: ServicesAccessor): void { + let config = accessor.get(IConfigurationService); + let value = BreadcrumbsConfig.IsEnabled.bindTo(config).getValue(); + BreadcrumbsConfig.IsEnabled.bindTo(config).updateValue(!value); } -}); -CommandsRegistry.registerCommand('breadcrumbs.toggle', accessor => { - let config = accessor.get(IConfigurationService); - let value = BreadcrumbsConfig.IsEnabled.bindTo(config).getValue(); - BreadcrumbsConfig.IsEnabled.bindTo(config).updateValue(!value); + }); // focus/focus-and-select @@ -530,20 +535,27 @@ function focusAndSelectHandler(accessor: ServicesAccessor, select: boolean): voi } } } -MenuRegistry.appendMenuItem(MenuId.CommandPalette, { - command: { - id: 'breadcrumbs.focusAndSelect', - title: { value: localize('cmd.focus', "Focus Breadcrumbs"), original: 'Focus Breadcrumbs' }, - precondition: BreadcrumbsControl.CK_BreadcrumbsVisible +registerAction2(class FocusAndSelectBreadcrumbs extends Action2 { + constructor() { + super({ + id: 'breadcrumbs.focusAndSelect', + title: { + value: localize('cmd.focus', "Focus Breadcrumbs"), + original: 'Focus Breadcrumbs' + }, + precondition: BreadcrumbsControl.CK_BreadcrumbsVisible, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Period, + when: BreadcrumbsControl.CK_BreadcrumbsPossible, + } + }); + } + run(accessor: ServicesAccessor, ...args: any[]): void { + focusAndSelectHandler(accessor, true); } }); -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: 'breadcrumbs.focusAndSelect', - weight: KeybindingWeight.WorkbenchContrib, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Period, - when: BreadcrumbsControl.CK_BreadcrumbsPossible, - handler: accessor => focusAndSelectHandler(accessor, true) -}); + KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'breadcrumbs.focus', weight: KeybindingWeight.WorkbenchContrib, diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index 318e7deb266..8e0c175392e 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -314,6 +314,9 @@ if (isMacintosh) { }); } +MenuRegistry.appendMenuItem(MenuId.TitleMenu, { command: { id: NavigateBackwardsAction.ID, title: NavigateBackwardsAction.LABEL, icon: Codicon.arrowLeft } }); +MenuRegistry.appendMenuItem(MenuId.TitleMenu, { command: { id: NavigateForwardAction.ID, title: NavigateForwardAction.LABEL, icon: Codicon.arrowRight } }); + // Empty Editor Group Toolbar MenuRegistry.appendMenuItem(MenuId.EmptyEditorGroup, { command: { id: UNLOCK_GROUP_COMMAND_ID, title: localize('unlockGroupAction', "Unlock Group"), icon: Codicon.lock }, group: 'navigation', order: 10, when: ActiveEditorGroupLockedContext }); MenuRegistry.appendMenuItem(MenuId.EmptyEditorGroup, { command: { id: CLOSE_EDITOR_GROUP_COMMAND_ID, title: localize('closeGroupAction', "Close Group"), icon: Codicon.close }, group: 'navigation', order: 20 }); diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index 0995d4e6e48..a993829e847 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -19,7 +19,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; -import { IFileDialogService, ConfirmResult } from 'vs/platform/dialogs/common/dialogs'; +import { IFileDialogService, ConfirmResult, IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { ItemActivation, IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { AllEditorsByMostRecentlyUsedQuickAccess, ActiveGroupEditorsByMostRecentlyUsedQuickAccess, AllEditorsByAppearanceQuickAccess } from 'vs/workbench/browser/parts/editor/editorQuickAccess'; import { Codicon } from 'vs/base/common/codicons'; @@ -1483,13 +1483,26 @@ export class ClearRecentFilesAction extends Action { id: string, label: string, @IWorkspacesService private readonly workspacesService: IWorkspacesService, - @IHistoryService private readonly historyService: IHistoryService + @IHistoryService private readonly historyService: IHistoryService, + @IDialogService private readonly dialogService: IDialogService ) { super(id, label); } override async run(): Promise<void> { + // Ask for confirmation + const { confirmed } = await this.dialogService.confirm({ + message: localize('confirmClearRecentsMessage', "Do you want to clear all recently opened files and workspaces?"), + detail: localize('confirmClearDetail', "This action is irreversible!"), + primaryButton: localize({ key: 'clearButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Clear"), + type: 'warning' + }); + + if (!confirmed) { + return; + } + // Clear global recently opened this.workspacesService.clearRecentlyOpened(); @@ -1746,14 +1759,27 @@ export class ClearEditorHistoryAction extends Action { constructor( id: string, label: string, - @IHistoryService private readonly historyService: IHistoryService + @IHistoryService private readonly historyService: IHistoryService, + @IDialogService private readonly dialogService: IDialogService ) { super(id, label); } override async run(): Promise<void> { - // Editor history + // Ask for confirmation + const { confirmed } = await this.dialogService.confirm({ + message: localize('confirmClearEditorHistoryMessage', "Do you want to clear the history of recently opened editors?"), + detail: localize('confirmClearDetail', "This action is irreversible!"), + primaryButton: localize({ key: 'clearButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Clear"), + type: 'warning' + }); + + if (!confirmed) { + return; + } + + // Clear editor history this.historyService.clear(); } } diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index cb67d57d434..3624936e41f 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -844,7 +844,8 @@ function registerCloseEditorCommands() { handler: (accessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { return Promise.all(getEditorsContext(accessor, resourceOrContext, context).groups.map(async group => { if (group) { - return group.closeAllEditors({ excludeSticky: true }); + await group.closeAllEditors({ excludeSticky: true }); + return; } })); } diff --git a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts index 2fa3f3998d6..0ee0ad2f093 100644 --- a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts +++ b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts @@ -4,34 +4,37 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/editordroptarget'; -import { Extensions as DragAndDropExtensions, LocalSelectionTransfer, DraggedEditorIdentifier, ResourcesDropHandler, DraggedEditorGroupIdentifier, containsDragType, CodeDataTransfers, DraggedTreeItemsIdentifier, extractTreeDropData, IDragAndDropContributionRegistry } from 'vs/workbench/browser/dnd'; -import { addDisposableListener, EventType, EventHelper, isAncestor, DragAndDropObserver } from 'vs/base/browser/dom'; -import { IEditorGroupsAccessor, IEditorGroupView, fillActiveEditorViewState } from 'vs/workbench/browser/parts/editor/editor'; -import { EDITOR_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme'; -import { IThemeService, Themable } from 'vs/platform/theme/common/themeService'; -import { activeContrastBorder } from 'vs/platform/theme/common/colorRegistry'; -import { IEditorIdentifier, EditorInputCapabilities, IUntypedEditorInput } from 'vs/workbench/common/editor'; -import { isMacintosh, isWeb } from 'vs/base/common/platform'; -import { GroupDirection, IEditorGroupsService, IMergeGroupOptions, MergeGroupMode } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { DataTransfers } from 'vs/base/browser/dnd'; +import { addDisposableListener, DragAndDropObserver, EventHelper, EventType, isAncestor } from 'vs/base/browser/dom'; +import { renderFormattedText } from 'vs/base/browser/formattedTextRenderer'; +import { RunOnceScheduler } from 'vs/base/common/async'; import { toDisposable } from 'vs/base/common/lifecycle'; +import { isMacintosh, isWeb } from 'vs/base/common/platform'; +import { assertAllDefined, assertIsDefined } from 'vs/base/common/types'; +import { localize } from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { RunOnceScheduler } from 'vs/base/common/async'; -import { DataTransfers } from 'vs/base/browser/dnd'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { activeContrastBorder } from 'vs/platform/theme/common/colorRegistry'; +import { IThemeService, Themable } from 'vs/platform/theme/common/themeService'; +import { isTemporaryWorkspace, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { CodeDataTransfers, containsDragType, DraggedEditorGroupIdentifier, DraggedEditorIdentifier, DraggedTreeItemsIdentifier, Extensions as DragAndDropExtensions, extractTreeDropData, IDragAndDropContributionRegistry, LocalSelectionTransfer, ResourcesDropHandler } from 'vs/workbench/browser/dnd'; +import { fillActiveEditorViewState, IEditorGroupsAccessor, IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; +import { EditorInputCapabilities, IEditorIdentifier, IUntypedEditorInput } from 'vs/workbench/common/editor'; +import { EDITOR_DRAG_AND_DROP_BACKGROUND, EDITOR_DROP_INTO_PROMPT_BACKGROUND, EDITOR_DROP_INTO_PROMPT_BORDER, EDITOR_DROP_INTO_PROMPT_FOREGROUND } from 'vs/workbench/common/theme'; +import { GroupDirection, IEditorGroupsService, IMergeGroupOptions, MergeGroupMode } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { assertIsDefined, assertAllDefined } from 'vs/base/common/types'; import { ITreeViewsService } from 'vs/workbench/services/views/browser/treeViewsService'; -import { isTemporaryWorkspace, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { Registry } from 'vs/platform/registry/common/platform'; interface IDropOperation { splitDirection?: GroupDirection; } -function isDragIntoEditorEvent(configurationService: IConfigurationService, e: DragEvent): boolean { - if (!configurationService.getValue<boolean>('workbench.experimental.editor.dragAndDropIntoEditor.enabled')) { - return false; - } +function isDropIntoEditorEnabledGlobally(configurationService: IConfigurationService) { + return configurationService.getValue<boolean>('workbench.experimental.editor.dropIntoEditor.enabled'); +} + +function isDragIntoEditorEvent(e: DragEvent): boolean { return e.shiftKey; } @@ -41,9 +44,12 @@ class DropOverlay extends Themable { private container: HTMLElement | undefined; private overlay: HTMLElement | undefined; + private dropIntoPromptElement?: HTMLSpanElement; private currentDropOperation: IDropOperation | undefined; + private _disposed: boolean | undefined; + get disposed(): boolean { return !!this._disposed; } private cleanupOverlayScheduler: RunOnceScheduler; @@ -51,6 +57,8 @@ class DropOverlay extends Themable { private readonly groupTransfer = LocalSelectionTransfer.getInstance<DraggedEditorGroupIdentifier>(); private readonly treeItemsTransfer = LocalSelectionTransfer.getInstance<DraggedTreeItemsIdentifier>(); + private readonly enableDropIntoEditor: boolean; + constructor( private accessor: IEditorGroupsAccessor, private groupView: IEditorGroupView, @@ -66,11 +74,9 @@ class DropOverlay extends Themable { this.cleanupOverlayScheduler = this._register(new RunOnceScheduler(() => this.dispose(), 300)); - this.create(); - } + this.enableDropIntoEditor = isDropIntoEditorEnabledGlobally(this.configurationService) && this.isDropIntoActiveEditorEnabled(); - get disposed(): boolean { - return !!this._disposed; + this.create(); } private create(): void { @@ -94,6 +100,12 @@ class DropOverlay extends Themable { this.overlay.classList.add('editor-group-overlay-indicator'); container.appendChild(this.overlay); + if (this.enableDropIntoEditor) { + this.dropIntoPromptElement = renderFormattedText(localize('dropIntoEditorPrompt', "Hold __{0}__ to drop into editor", isMacintosh ? '⇧' : 'Shift'), {}); + this.dropIntoPromptElement.classList.add('editor-group-overlay-drop-into-prompt'); + this.overlay.appendChild(this.dropIntoPromptElement); + } + // Overlay Event Handling this.registerListeners(container); @@ -113,13 +125,27 @@ class DropOverlay extends Themable { overlay.style.outlineOffset = activeContrastBorderColor ? '-2px' : ''; overlay.style.outlineStyle = activeContrastBorderColor ? 'dashed' : ''; overlay.style.outlineWidth = activeContrastBorderColor ? '2px' : ''; + + if (this.dropIntoPromptElement) { + this.dropIntoPromptElement.style.backgroundColor = this.getColor(EDITOR_DROP_INTO_PROMPT_BACKGROUND) ?? ''; + this.dropIntoPromptElement.style.color = this.getColor(EDITOR_DROP_INTO_PROMPT_FOREGROUND) ?? ''; + + const borderColor = this.getColor(EDITOR_DROP_INTO_PROMPT_BORDER); + if (borderColor) { + this.dropIntoPromptElement.style.borderWidth = '1px'; + this.dropIntoPromptElement.style.borderStyle = 'solid'; + this.dropIntoPromptElement.style.borderColor = borderColor; + } else { + this.dropIntoPromptElement.style.borderWidth = '0'; + } + } } private registerListeners(container: HTMLElement): void { this._register(new DragAndDropObserver(container, { onDragEnter: e => undefined, onDragOver: e => { - if (isDragIntoEditorEvent(this.configurationService, e)) { + if (this.enableDropIntoEditor && isDragIntoEditorEvent(e)) { this.dispose(); return; } @@ -199,6 +225,10 @@ class DropOverlay extends Themable { })); } + private isDropIntoActiveEditorEnabled(): boolean { + return !!this.groupView.activeEditor?.hasCapability(EditorInputCapabilities.CanDropIntoEditor); + } + private findSourceGroupView(): IEditorGroupView | undefined { // Check for group transfer @@ -444,18 +474,23 @@ class DropOverlay extends Themable { switch (splitDirection) { case GroupDirection.UP: this.doPositionOverlay({ top: '0', left: '0', width: '100%', height: '50%' }); + this.toggleDropIntoPrompt(false); break; case GroupDirection.DOWN: this.doPositionOverlay({ top: '50%', left: '0', width: '100%', height: '50%' }); + this.toggleDropIntoPrompt(false); break; case GroupDirection.LEFT: this.doPositionOverlay({ top: '0', left: '0', width: '50%', height: '100%' }); + this.toggleDropIntoPrompt(false); break; case GroupDirection.RIGHT: this.doPositionOverlay({ top: '0', left: '50%', width: '50%', height: '100%' }); + this.toggleDropIntoPrompt(false); break; default: this.doPositionOverlay({ top: '0', left: '0', width: '100%', height: '100%' }); + this.toggleDropIntoPrompt(true); } // Make sure the overlay is visible now @@ -510,6 +545,13 @@ class DropOverlay extends Themable { this.currentDropOperation = undefined; } + private toggleDropIntoPrompt(showing: boolean) { + if (!this.dropIntoPromptElement) { + return; + } + this.dropIntoPromptElement.style.opacity = showing ? '1' : '0'; + } + contains(element: HTMLElement): boolean { return element === this.container || element === this.overlay; } @@ -566,7 +608,7 @@ export class EditorDropTarget extends Themable { } private onDragEnter(event: DragEvent): void { - if (isDragIntoEditorEvent(this.configurationService, event)) { + if (isDropIntoEditorEnabledGlobally(this.configurationService) && isDragIntoEditorEvent(event)) { return; } diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 0286fc0b08f..7ea0ca109d4 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -9,7 +9,7 @@ import { GroupIdentifier, CloseDirection, IEditorCloseEvent, IEditorPane, SaveRe import { ActiveEditorGroupLockedContext, ActiveEditorDirtyContext, EditorGroupEditorsCountContext, ActiveEditorStickyContext, ActiveEditorPinnedContext, ActiveEditorLastInGroupContext, ActiveEditorFirstInGroupContext } from 'vs/workbench/common/contextkeys'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; -import { Event, Emitter, Relay } from 'vs/base/common/event'; +import { Emitter, Relay } from 'vs/base/common/event'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Dimension, trackFocus, addDisposableListener, EventType, EventHelper, findParentWithClass, clearNode, isAncestor, asCSSUrl } from 'vs/base/browser/dom'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; @@ -26,10 +26,7 @@ import { IEditorProgressService } from 'vs/platform/progress/common/progress'; import { EditorProgressIndicator } from 'vs/workbench/services/progress/browser/progressIndicator'; import { localize } from 'vs/nls'; import { coalesce, firstOrDefault } from 'vs/base/common/arrays'; -import { isCancellationError } from 'vs/base/common/errors'; import { combinedDisposable, dispose, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { Severity, INotificationService } from 'vs/platform/notification/common/notification'; -import { toErrorMessage, isErrorWithActions } from 'vs/base/common/errorMessage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { DeferredPromise, Promises, RunOnceWorker } from 'vs/base/common/async'; import { EventType as TouchEventType, GestureEvent } from 'vs/base/browser/touch'; @@ -48,9 +45,8 @@ import { hash } from 'vs/base/common/hash'; import { getMimeTypes } from 'vs/editor/common/services/languagesAssociations'; import { extname, isEqual } from 'vs/base/common/resources'; import { FileAccess, Schemas } from 'vs/base/common/network'; -import { EditorActivation, EditorOpenSource, IEditorOptions } from 'vs/platform/editor/common/editor'; -import { IDialogService, IFileDialogService, ConfirmResult } from 'vs/platform/dialogs/common/dialogs'; -import { ILogService } from 'vs/platform/log/common/log'; +import { EditorActivation, IEditorOptions } from 'vs/platform/editor/common/editor'; +import { IFileDialogService, ConfirmResult } from 'vs/platform/dialogs/common/dialogs'; import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { withNullAsUndefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; @@ -134,7 +130,6 @@ export class EditorGroupView extends Themable implements IEditorGroupView { private readonly whenRestoredPromise = new DeferredPromise<void>(); readonly whenRestored = this.whenRestoredPromise.p; - private isRestored = false; constructor( private accessor: IEditorGroupsAccessor, @@ -143,14 +138,11 @@ export class EditorGroupView extends Themable implements IEditorGroupView { @IInstantiationService private readonly instantiationService: IInstantiationService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IThemeService themeService: IThemeService, - @INotificationService private readonly notificationService: INotificationService, - @IDialogService private readonly dialogService: IDialogService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IKeybindingService private readonly keybindingService: IKeybindingService, @IMenuService private readonly menuService: IMenuService, @IContextMenuService private readonly contextMenuService: IContextMenuService, @IFileDialogService private readonly fileDialogService: IFileDialogService, - @ILogService private readonly logService: ILogService, @IEditorService private readonly editorService: EditorServiceImpl, @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService @@ -235,7 +227,6 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Signal restored once editors have restored restoreEditorsPromise.finally(() => { - this.isRestored = true; this.whenRestoredPromise.complete(); }); @@ -1105,9 +1096,9 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this._onDidActiveEditorChange.fire({ editor }); } - // Handle errors but do not bubble them up + // Indicate error as an event but do not bubble them up if (error) { - await this.doHandleOpenEditorError(error, editor, options); + this._onDidOpenEditorFail.fire(editor); } // Without an editor pane, recover by closing the active editor @@ -1132,84 +1123,6 @@ export class EditorGroupView extends Themable implements IEditorGroupView { return openEditorPromise; } - private async doHandleOpenEditorError(error: Error, editor: EditorInput, options?: IEditorOptions): Promise<void> { - - // Report error only if we are not told to ignore errors that occur from opening an editor - if (!isCancellationError(error) && !options?.ignoreError) { - - // Always log the error to figure out what is going on - this.logService.error(error); - - // Since it is more likely that errors fail to open when restoring them e.g. - // because files got deleted or moved meanwhile, we do not show any notifications - // if we are still restoring editors. - if (this.isRestored) { - - // Extract possible error actions from the error - let errorActions: readonly IAction[] | undefined = undefined; - if (isErrorWithActions(error)) { - errorActions = error.actions; - } - - // If the context is USER, we try to show a modal dialog instead of a background notification - if (options?.source === EditorOpenSource.USER) { - const buttons: string[] = []; - if (Array.isArray(errorActions) && errorActions.length > 0) { - for (const errorAction of errorActions) { - buttons.push(errorAction.label); - } - } else { - buttons.push(localize('ok', 'OK')); - } - - let cancelId: number | undefined = undefined; - if (buttons.length === 1) { - buttons.push(localize('cancel', "Cancel")); - cancelId = 1; - } - - const result = await this.dialogService.show( - Severity.Error, - localize('editorOpenErrorDialog', "Unable to open '{0}'", editor.getName()), - buttons, - { - detail: toErrorMessage(error), - cancelId - } - ); - - // Make sure to run any error action if present - if (result.choice !== cancelId && Array.isArray(errorActions)) { - const errorAction = errorActions[result.choice]; - if (errorAction) { - errorAction.run(); - } - } - } - - // Otherwise, show a background notification. - else { - const actions = { primary: [] as readonly IAction[] }; - if (Array.isArray(errorActions)) { - actions.primary = errorActions; - } - - const handle = this.notificationService.notify({ - id: `${hash(editor.resource?.toString())}`, // unique per editor - severity: Severity.Error, - message: localize('editorOpenError', "Unable to open '{0}': {1}.", editor.getName(), toErrorMessage(error)), - actions - }); - - Event.once(handle.onDidClose)(() => actions.primary && dispose(actions.primary)); - } - } - } - - // Event - this._onDidOpenEditorFail.fire(editor); - } - //#endregion //#region openEditors() @@ -1786,7 +1699,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { //#region closeAllEditors() - async closeAllEditors(options?: ICloseAllEditorsOptions): Promise<void> { + async closeAllEditors(options?: ICloseAllEditorsOptions): Promise<boolean> { if (this.isEmpty) { // If the group is empty and the request is to close all editors, we still close @@ -1796,17 +1709,19 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this.accessor.removeGroup(this); } - return; + return true; } // Check for dirty and veto const veto = await this.handleDirtyClosing(this.model.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE, options)); if (veto) { - return; + return false; } // Do close this.doCloseAllEditors(options); + + return true; } private doCloseAllEditors(options?: ICloseAllEditorsOptions): void { diff --git a/src/vs/workbench/browser/parts/editor/editorPanes.ts b/src/vs/workbench/browser/parts/editor/editorPanes.ts index a587913b667..268c214a232 100644 --- a/src/vs/workbench/browser/parts/editor/editorPanes.ts +++ b/src/vs/workbench/browser/parts/editor/editorPanes.ts @@ -3,6 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from 'vs/nls'; +import { IAction } from 'vs/base/common/actions'; +import { Emitter } from 'vs/base/common/event'; +import Severity from 'vs/base/common/severity'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { EditorExtensions, EditorInputCapabilities, IEditorOpenContext, IVisibleEditorPane } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; @@ -14,12 +18,14 @@ import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IEditorProgressService, LongRunningOperation } from 'vs/platform/progress/common/progress'; import { IEditorGroupView, DEFAULT_EDITOR_MIN_DIMENSIONS, DEFAULT_EDITOR_MAX_DIMENSIONS } from 'vs/workbench/browser/parts/editor/editor'; -import { Emitter } from 'vs/base/common/event'; import { assertIsDefined } from 'vs/base/common/types'; import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; -import { UnavailableResourceErrorEditor, UnknownErrorEditor, WorkspaceTrustRequiredEditor } from 'vs/workbench/browser/parts/editor/editorPlaceholder'; -import { IEditorOptions } from 'vs/platform/editor/common/editor'; -import { FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; +import { ErrorPlaceholderEditor, IErrorEditorPlaceholderOptions, WorkspaceTrustRequiredPlaceholderEditor } from 'vs/workbench/browser/parts/editor/editorPlaceholder'; +import { EditorOpenSource, IEditorOptions } from 'vs/platform/editor/common/editor'; +import { isCancellationError } from 'vs/base/common/errors'; +import { isErrorWithActions, toErrorMessage } from 'vs/base/common/errorMessage'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; export interface IOpenEditorResult { @@ -88,7 +94,9 @@ export class EditorPanes extends Disposable { @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IEditorProgressService private readonly editorProgressService: IEditorProgressService, - @IWorkspaceTrustManagementService private readonly workspaceTrustService: IWorkspaceTrustManagementService + @IWorkspaceTrustManagementService private readonly workspaceTrustService: IWorkspaceTrustManagementService, + @ILogService private readonly logService: ILogService, + @IDialogService private readonly dialogService: IDialogService ) { super(); @@ -118,26 +126,91 @@ export class EditorPanes extends Disposable { return await this.doOpenEditor(this.getEditorPaneDescriptor(editor), editor, options, context); } catch (error) { - // We failed to open an editor and thus fallback to show a generic - // editor in error state as a way for the user to be aware. - // Previously we would immediately close the editor and show a - // error notification which was easy to not see. - // - // Besides, we want to preserve the users editor UI state as much - // as possible, so closing editors is never really an option. + // First check if caller instructed us to ignore error handling + if (options?.ignoreError) { + return { error }; + } + + // In case of an error when opening an editor, we still want to show + // an editor in the desired location to preserve the user intent and + // view state (e.g. when restoring). // - // Related issues: - // - https://github.com/microsoft/vscode/issues/110062 - // - https://github.com/microsoft/vscode/issues/142875 + // For that reason we have place holder editors that can convey a + // message with actions the user can click on. + + return this.doShowError(error, editor, options, context); + } + } + + private async doShowError(error: Error, editor: EditorInput, options?: IEditorOptions, context?: IEditorOpenContext): Promise<IOpenEditorResult> { + + // Always log the error to figure out what is going on + this.logService.error(error); + + // Show as modal dialog when explicit user action + let errorHandled = false; + if (options?.source === EditorOpenSource.USER) { - const isUnavailableResource = (<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND; - const editorPlaceholder = isUnavailableResource ? UnavailableResourceErrorEditor.DESCRIPTOR : UnknownErrorEditor.DESCRIPTOR; + // Extract possible error actions from the error + let errorActions: readonly IAction[] | undefined = undefined; + if (isErrorWithActions(error)) { + errorActions = error.actions; + } + + const buttons: string[] = []; + if (errorActions && errorActions.length > 0) { + for (const errorAction of errorActions) { + buttons.push(errorAction.label); + } + } else { + buttons.push(localize('ok', 'OK')); + } - return { - ...(await this.doOpenEditor(editorPlaceholder, editor, options, context)), - error - }; + let cancelId: number | undefined = undefined; + if (buttons.length === 1) { + buttons.push(localize('cancel', "Cancel")); + cancelId = 1; + } + + const result = await this.dialogService.show( + Severity.Error, + localize('editorOpenErrorDialog', "Unable to open '{0}'", editor.getName()), + buttons, + { + detail: toErrorMessage(error), + cancelId + } + ); + + // Make sure to run any error action if present + if (result.choice !== cancelId && errorActions) { + const errorAction = errorActions[result.choice]; + if (errorAction) { + const result = errorAction.run(); + if (result instanceof Promise) { + result.catch(error => this.dialogService.show(Severity.Error, toErrorMessage(error))); + } + + errorHandled = true; // consider the error as handled! + } + } } + + // Return early if the user dealt with the error already + if (errorHandled) { + return { error }; + } + + // Show as editor placeholder: pass over the error to display + const editorPlaceholderOptions: IErrorEditorPlaceholderOptions = { ...options }; + if (!isCancellationError(error)) { + editorPlaceholderOptions.error = error; + } + + return { + ...(await this.doOpenEditor(ErrorPlaceholderEditor.DESCRIPTOR, editor, editorPlaceholderOptions, context)), + error + }; } private async doOpenEditor(descriptor: IEditorPaneDescriptor, editor: EditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext = Object.create(null)): Promise<IOpenEditorResult> { @@ -165,7 +238,7 @@ export class EditorPanes extends Disposable { // but the current workspace is untrusted, we fallback to a generic // editor descriptor to indicate this an do NOT load the registered // editor. - return WorkspaceTrustRequiredEditor.DESCRIPTOR; + return WorkspaceTrustRequiredPlaceholderEditor.DESCRIPTOR; } return assertIsDefined(this.editorPanesRegistry.getEditorPane(editor)); diff --git a/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts b/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts index af524fb057f..0464f14265b 100644 --- a/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts +++ b/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts @@ -5,14 +5,15 @@ import 'vs/css!./media/editorplaceholder'; import { localize } from 'vs/nls'; +import Severity from 'vs/base/common/severity'; import { IEditorOpenContext } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import { ScrollbarVisibility } from 'vs/base/common/scrollable'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { Dimension, size, clearNode } from 'vs/base/browser/dom'; +import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { Dimension, size, clearNode, $ } from 'vs/base/browser/dom'; import { CancellationToken } from 'vs/base/common/cancellation'; import { DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { IStorageService } from 'vs/platform/storage/common/storage'; @@ -23,8 +24,29 @@ import { EditorOpenSource, IEditorOptions } from 'vs/platform/editor/common/edit import { computeEditorAriaLabel, EditorPaneDescriptor } from 'vs/workbench/browser/editor'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Link } from 'vs/platform/opener/browser/link'; +import { SimpleIconLabel } from 'vs/base/browser/ui/iconLabel/simpleIconLabel'; +import { editorErrorForeground, editorInfoForeground, editorWarningForeground } from 'vs/platform/theme/common/colorRegistry'; +import { Codicon } from 'vs/base/common/codicons'; +import { FileChangeType, FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files'; +import { isErrorWithActions, toErrorMessage } from 'vs/base/common/errorMessage'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; + +export interface IEditorPlaceholderContents { + icon: string; + label: string; + actions: IEditorPlaceholderContentsAction[]; +} + +export interface IEditorPlaceholderContentsAction { + label: string; + run: () => unknown; +} -abstract class EditorPlaceholderPane extends EditorPane { +export interface IErrorEditorPlaceholderOptions extends IEditorOptions { + error?: Error; +} + +export abstract class EditorPlaceholder extends EditorPane { private container: HTMLElement | undefined; private scrollbar: DomScrollableElement | undefined; @@ -32,18 +54,14 @@ abstract class EditorPlaceholderPane extends EditorPane { constructor( id: string, - private readonly title: string, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, - @IStorageService storageService: IStorageService + @IStorageService storageService: IStorageService, + @IInstantiationService private readonly instantiationService: IInstantiationService ) { super(id, telemetryService, themeService, storageService); } - override getTitle(): string { - return this.title; - } - protected createEditor(parent: HTMLElement): void { // Container @@ -66,21 +84,43 @@ abstract class EditorPlaceholderPane extends EditorPane { } // Render Input - this.inputDisposable.value = this.renderInput(input); + this.inputDisposable.value = await this.renderInput(input, options); } - private renderInput(input: EditorInput): IDisposable { + private async renderInput(input: EditorInput, options: IEditorOptions | undefined): Promise<IDisposable> { const [container, scrollbar] = assertAllDefined(this.container, this.scrollbar); // Reset any previous contents clearNode(container); - // Update ARIA label - container.setAttribute('aria-label', computeEditorAriaLabel(input, undefined, this.group, undefined)); - - // Delegate to implementation + // Delegate to implementation for contents const disposables = new DisposableStore(); - this.renderBody(container, disposables); + const { icon, label, actions } = await this.getContents(input, options, disposables); + + // Icon + const iconContainer = container.appendChild($('.editor-placeholder-icon-container')); + const iconWidget = new SimpleIconLabel(iconContainer); + iconWidget.text = icon; + + // Label + const labelContainer = container.appendChild($('.editor-placeholder-label-container')); + const labelWidget = document.createElement('span'); + labelWidget.textContent = label; + labelContainer.appendChild(labelWidget); + + // ARIA label + container.setAttribute('aria-label', `${computeEditorAriaLabel(input, undefined, this.group, undefined)}, ${label}`); + + // Actions + const actionsContainer = container.appendChild($('.editor-placeholder-actions-container')); + for (const action of actions) { + disposables.add(this.instantiationService.createInstance(Link, actionsContainer, { + label: action.label, + href: '' + }, { + opener: () => action.run() + })); + } // Adjust scrollbar scrollbar.scanDomNode(); @@ -88,7 +128,7 @@ abstract class EditorPlaceholderPane extends EditorPane { return disposables; } - protected abstract renderBody(container: HTMLElement, disposables: DisposableStore): void; + protected abstract getContents(input: EditorInput, options: IEditorOptions | undefined, disposables: DisposableStore): Promise<IEditorPlaceholderContents>; override clearInput(): void { if (this.container) { @@ -108,6 +148,9 @@ abstract class EditorPlaceholderPane extends EditorPane { // Adjust scrollbar scrollbar.scanDomNode(); + + // Toggle responsive class + container.classList.toggle('max-height-200px', dimension.height <= 200); } override focus(): void { @@ -123,11 +166,12 @@ abstract class EditorPlaceholderPane extends EditorPane { } } -export class WorkspaceTrustRequiredEditor extends EditorPlaceholderPane { +export class WorkspaceTrustRequiredPlaceholderEditor extends EditorPlaceholder { static readonly ID = 'workbench.editors.workspaceTrustRequiredEditor'; - static readonly LABEL = localize('trustRequiredEditor', "Workspace Trust Required"); - static readonly DESCRIPTOR = EditorPaneDescriptor.create(WorkspaceTrustRequiredEditor, WorkspaceTrustRequiredEditor.ID, WorkspaceTrustRequiredEditor.LABEL); + private static readonly LABEL = localize('trustRequiredEditor', "Workspace Trust Required"); + + static readonly DESCRIPTOR = EditorPaneDescriptor.create(WorkspaceTrustRequiredPlaceholderEditor, WorkspaceTrustRequiredPlaceholderEditor.ID, WorkspaceTrustRequiredPlaceholderEditor.LABEL); constructor( @ITelemetryService telemetryService: ITelemetryService, @@ -135,95 +179,128 @@ export class WorkspaceTrustRequiredEditor extends EditorPlaceholderPane { @ICommandService private readonly commandService: ICommandService, @IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService, @IStorageService storageService: IStorageService, - @IInstantiationService private readonly instantiationService: IInstantiationService + @IInstantiationService instantiationService: IInstantiationService ) { - super(WorkspaceTrustRequiredEditor.ID, WorkspaceTrustRequiredEditor.LABEL, telemetryService, themeService, storageService); + super(WorkspaceTrustRequiredPlaceholderEditor.ID, telemetryService, themeService, storageService, instantiationService); } - protected renderBody(container: HTMLElement, disposables: DisposableStore): void { - const label = container.appendChild(document.createElement('p')); - label.textContent = isSingleFolderWorkspaceIdentifier(toWorkspaceIdentifier(this.workspaceService.getWorkspace())) ? - localize('requiresFolderTrustText', "The file is not displayed in the editor because trust has not been granted to the folder.") : - localize('requiresWorkspaceTrustText', "The file is not displayed in the editor because trust has not been granted to the workspace."); + override getTitle(): string { + return WorkspaceTrustRequiredPlaceholderEditor.LABEL; + } - disposables.add(this.instantiationService.createInstance(Link, label, { - label: localize('manageTrust', "Manage Workspace Trust"), - href: '' - }, { - opener: () => this.commandService.executeCommand('workbench.trust.manage') - })); + protected async getContents(): Promise<IEditorPlaceholderContents> { + return { + icon: '$(workspace-untrusted)', + label: isSingleFolderWorkspaceIdentifier(toWorkspaceIdentifier(this.workspaceService.getWorkspace())) ? + localize('requiresFolderTrustText', "The file is not displayed in the editor because trust has not been granted to the folder.") : + localize('requiresWorkspaceTrustText', "The file is not displayed in the editor because trust has not been granted to the workspace."), + actions: [ + { + label: localize('manageTrust', "Manage Workspace Trust"), + run: () => this.commandService.executeCommand('workbench.trust.manage') + } + ] + }; } } -abstract class AbstractErrorEditor extends EditorPlaceholderPane { +export class ErrorPlaceholderEditor extends EditorPlaceholder { + + private static readonly ID = 'workbench.editors.errorEditor'; + private static readonly LABEL = localize('errorEditor', "Error Editor"); + + static readonly DESCRIPTOR = EditorPaneDescriptor.create(ErrorPlaceholderEditor, ErrorPlaceholderEditor.ID, ErrorPlaceholderEditor.LABEL); constructor( - id: string, - label: string, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IStorageService storageService: IStorageService, - @IInstantiationService private readonly instantiationService: IInstantiationService + @IInstantiationService instantiationService: IInstantiationService, + @IFileService private readonly fileService: IFileService, + @IDialogService private readonly dialogService: IDialogService ) { - super(id, label, telemetryService, themeService, storageService); + super(ErrorPlaceholderEditor.ID, telemetryService, themeService, storageService, instantiationService); } - protected abstract getErrorMessage(): string; - - protected renderBody(container: HTMLElement, disposables: DisposableStore): void { - const label = container.appendChild(document.createElement('p')); - label.textContent = this.getErrorMessage(); - - // Offer to re-open + protected async getContents(input: EditorInput, options: IErrorEditorPlaceholderOptions, disposables: DisposableStore): Promise<IEditorPlaceholderContents> { + const resource = input.resource; const group = this.group; - const input = this.input; - if (group && input) { - disposables.add(this.instantiationService.createInstance(Link, label, { - label: localize('retry', "Try Again"), - href: '' - }, { - opener: () => group.openEditor(input, { ...this.options, source: EditorOpenSource.USER /* explicit user gesture */ }) - })); + const error = options.error; + const isFileNotFound = (<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND; + + // Error Label + let label: string; + if (isFileNotFound) { + label = localize('unavailableResourceErrorEditorText', "The editor could not be opened because the file was not found."); + } else if (error) { + label = localize('unknownErrorEditorTextWithError', "The editor could not be opened due to an unexpected error: {0}", toErrorMessage(error)); + } else { + label = localize('unknownErrorEditorTextWithoutError', "The editor could not be opened due to an unexpected error."); } - } -} - -export class UnknownErrorEditor extends AbstractErrorEditor { - static readonly ID = 'workbench.editors.unknownErrorEditor'; - static readonly LABEL = localize('unknownErrorEditor', "Unknown Error Editor"); - static readonly DESCRIPTOR = EditorPaneDescriptor.create(UnknownErrorEditor, UnknownErrorEditor.ID, UnknownErrorEditor.LABEL); + // Actions + let actions: IEditorPlaceholderContentsAction[] | undefined = undefined; + if (isErrorWithActions(error) && error.actions.length > 0) { + actions = error.actions.map(action => { + return { + label: action.label, + run: () => { + const result = action.run(); + if (result instanceof Promise) { + result.catch(error => this.dialogService.show(Severity.Error, toErrorMessage(error))); + } + } + }; + }); + } else if (group) { + actions = [ + { + label: localize('retry', "Try Again"), + run: () => group.openEditor(input, { ...options, source: EditorOpenSource.USER /* explicit user gesture */ }) + } + ]; + } - constructor( - @ITelemetryService telemetryService: ITelemetryService, - @IThemeService themeService: IThemeService, - @IStorageService storageService: IStorageService, - @IInstantiationService instantiationService: IInstantiationService - ) { - super(UnknownErrorEditor.ID, UnknownErrorEditor.LABEL, telemetryService, themeService, storageService, instantiationService); - } + // Auto-reload when file is added + if (group && isFileNotFound && resource && this.fileService.hasProvider(resource)) { + disposables.add(this.fileService.onDidFilesChange(e => { + if (e.contains(resource, FileChangeType.ADDED, FileChangeType.UPDATED)) { + group.openEditor(input, options); + } + })); + } - protected override getErrorMessage(): string { - return localize('unknownErrorEditorText', "The editor could not be opened due to an unexpected error."); + return { icon: '$(error)', label, actions: actions ?? [] }; } } -export class UnavailableResourceErrorEditor extends AbstractErrorEditor { +registerThemingParticipant((theme, collector) => { - static readonly ID = 'workbench.editors.unavailableResourceErrorEditor'; - static readonly LABEL = localize('unavailableResourceErrorEditor', "Unavailable Resource Error Editor"); - static readonly DESCRIPTOR = EditorPaneDescriptor.create(UnavailableResourceErrorEditor, UnavailableResourceErrorEditor.ID, UnavailableResourceErrorEditor.LABEL); + // Editor Placeholder Error Icon + const editorErrorIconForegroundColor = theme.getColor(editorErrorForeground); + if (editorErrorIconForegroundColor) { + collector.addRule(` + .monaco-editor-pane-placeholder .editor-placeholder-icon-container ${Codicon.error.cssSelector} { + color: ${editorErrorIconForegroundColor}; + }`); + } - constructor( - @ITelemetryService telemetryService: ITelemetryService, - @IThemeService themeService: IThemeService, - @IStorageService storageService: IStorageService, - @IInstantiationService instantiationService: IInstantiationService - ) { - super(UnavailableResourceErrorEditor.ID, UnavailableResourceErrorEditor.LABEL, telemetryService, themeService, storageService, instantiationService); + // Editor Placeholder Warning Icon + const editorWarningIconForegroundColor = theme.getColor(editorWarningForeground); + if (editorWarningIconForegroundColor) { + collector.addRule(` + .monaco-editor-pane-placeholder .editor-placeholder-icon-container ${Codicon.warning.cssSelector} { + color: ${editorWarningIconForegroundColor}; + }`); } - protected override getErrorMessage(): string { - return localize('unavailableResourceErrorEditorText', "The editor could not be opened because the file was not found."); + // Editor Placeholder Info/Trust Icon + const editorInfoIconForegroundColor = theme.getColor(editorInfoForeground); + if (editorInfoIconForegroundColor) { + collector.addRule(` + .monaco-editor-pane-placeholder .editor-placeholder-icon-container ${Codicon.info.cssSelector}, + .monaco-editor-pane-placeholder .editor-placeholder-icon-container ${Codicon.workspaceUntrusted.cssSelector} { + color: ${editorInfoIconForegroundColor}; + }`); } -} +}); diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index 92091e6054d..7571481ae97 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -1202,7 +1202,7 @@ export class ChangeLanguageAction extends Action { if (resource) { // Detect languages since we are in an untitled file let languageId: string | undefined = withNullAsUndefined(this.languageService.guessLanguageIdByFilepathOrFirstLine(resource, textModel.getLineContent(1))); - if (!languageId) { + if (!languageId || languageId === 'unknown') { detectedLanguage = await this.languageDetectionService.detectLanguage(resource); languageId = detectedLanguage; } diff --git a/src/vs/workbench/browser/parts/editor/media/editordroptarget.css b/src/vs/workbench/browser/parts/editor/media/editordroptarget.css index d21f31445b3..8a50b279785 100644 --- a/src/vs/workbench/browser/parts/editor/media/editordroptarget.css +++ b/src/vs/workbench/browser/parts/editor/media/editordroptarget.css @@ -20,6 +20,28 @@ pointer-events: none; /* very important to not take events away from the parent */ opacity: 0; /* hidden initially */ transition: opacity 150ms ease-out; + + display: flex; + align-items: center; + justify-content: center; +} + +#monaco-workbench-editor-drop-overlay .editor-group-overlay-drop-into-prompt { + text-align: center; + padding: 0.6em; + margin: 0.2em; + line-height: normal; + opacity: 0; /* hidden initially */ + transition: opacity 150ms ease-out; +} + +#monaco-workbench-editor-drop-overlay .editor-group-overlay-drop-into-prompt i /* Style keybinding */ { + padding: 0 8px; + border: 1px solid hsla(0,0%,80%,.4); + margin: 0 1px; + border-radius: 5px; + background-color: rgba(255, 255, 255, 0.05); + font-style: normal; } #monaco-workbench-editor-drop-overlay > .editor-group-overlay-indicator.overlay-move-transition { diff --git a/src/vs/workbench/browser/parts/editor/media/editorplaceholder.css b/src/vs/workbench/browser/parts/editor/media/editorplaceholder.css index a066aaec52c..692baa8c240 100644 --- a/src/vs/workbench/browser/parts/editor/media/editorplaceholder.css +++ b/src/vs/workbench/browser/parts/editor/media/editorplaceholder.css @@ -3,17 +3,38 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +.monaco-editor-pane-placeholder { + padding: 0 16px 0 16px; + box-sizing: border-box; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 10px; +} + .monaco-editor-pane-placeholder:focus { outline: none !important; } -.monaco-editor-pane-placeholder { - padding: 0 0 0 16px; - box-sizing: border-box; +.monaco-editor-pane-placeholder .editor-placeholder-icon-container .codicon { + font-size: 48px; +} + +.monaco-editor-pane-placeholder.max-height-200px .editor-placeholder-icon-container { + /* Hide the icon when height is limited */ + display: none; +} + +.monaco-editor-pane-placeholder .editor-placeholder-label-container { + font-size: 14px; + max-width: 450px; + text-align: center } .monaco-editor-pane-placeholder .monaco-link, .monaco-editor-pane-placeholder .monaco-link:hover { + font-size: 14px; cursor: pointer; text-decoration: underline; margin-left: 5px; diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index 5da06c5f378..6d834b41807 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -62,7 +62,7 @@ interface EditorInputLabel { ariaLabel?: string; } -interface ITabsTitleControlLayoutOtions { +interface ITabsTitleControlLayoutOptions { /** * Whether to force revealing the active tab, even when @@ -77,7 +77,7 @@ interface IScheduledTabsTitleControlLayout extends IDisposable { /** * Associated options with the layout call. */ - options?: ITabsTitleControlLayoutOtions; + options?: ITabsTitleControlLayoutOptions; } type EditorInputLabelAndEditor = EditorInputLabel & { editor: EditorInput }; @@ -1078,11 +1078,15 @@ export class TabsTitleControl extends TitleControl { } } - private redraw(options?: ITabsTitleControlLayoutOtions): void { + private redraw(options?: ITabsTitleControlLayoutOptions): void { - // Border below tabs if any - const tabsContainerBorderColor = this.getColor(EDITOR_GROUP_HEADER_TABS_BORDER); + // Border below tabs if any with explicit high contrast support if (this.tabsAndActionsContainer) { + let tabsContainerBorderColor = this.getColor(EDITOR_GROUP_HEADER_TABS_BORDER); + if (!tabsContainerBorderColor && isHighContrast(this.theme.type)) { + tabsContainerBorderColor = this.getColor(TAB_BORDER) || this.getColor(contrastBorder); + } + if (tabsContainerBorderColor) { this.tabsAndActionsContainer.classList.add('tabs-border-bottom'); this.tabsAndActionsContainer.style.setProperty('--tabs-border-bottom-color', tabsContainerBorderColor.toString()); @@ -1386,7 +1390,7 @@ export class TabsTitleControl extends TitleControl { return { total, offset }; } - layout(dimensions: ITitleControlDimensions, options?: ITabsTitleControlLayoutOtions): Dimension { + layout(dimensions: ITitleControlDimensions, options?: ITabsTitleControlLayoutOptions): Dimension { // Remember dimensions that we get Object.assign(this.dimensions, dimensions); @@ -1420,7 +1424,7 @@ export class TabsTitleControl extends TitleControl { return this.dimensions.used; } - private doLayout(dimensions: ITitleControlDimensions, options?: ITabsTitleControlLayoutOtions): void { + private doLayout(dimensions: ITitleControlDimensions, options?: ITabsTitleControlLayoutOptions): void { // Only layout if we have valid tab index and dimensions const activeTabAndIndex = this.group.activeEditor ? this.getTabAndIndex(this.group.activeEditor) : undefined; @@ -1459,7 +1463,7 @@ export class TabsTitleControl extends TitleControl { } } - private doLayoutTabs(activeTab: HTMLElement, activeIndex: number, dimensions: ITitleControlDimensions, options?: ITabsTitleControlLayoutOtions): void { + private doLayoutTabs(activeTab: HTMLElement, activeIndex: number, dimensions: ITitleControlDimensions, options?: ITabsTitleControlLayoutOptions): void { // Always first layout tabs with wrapping support even if wrapping // is disabled. The result indicates if tabs wrap and if not, we @@ -1601,7 +1605,7 @@ export class TabsTitleControl extends TitleControl { return tabsWrapMultiLine; } - private doLayoutTabsNonWrapping(activeTab: HTMLElement, activeIndex: number, options?: ITabsTitleControlLayoutOtions): void { + private doLayoutTabsNonWrapping(activeTab: HTMLElement, activeIndex: number, options?: ITabsTitleControlLayoutOptions): void { const [tabsContainer, tabsScrollbar] = assertAllDefined(this.tabsContainer, this.tabsScrollbar); // @@ -1881,18 +1885,6 @@ export class TabsTitleControl extends TitleControl { registerThemingParticipant((theme, collector) => { - // Add border between tabs and breadcrumbs in high contrast mode. - if (isHighContrast(theme.type)) { - const borderColor = (theme.getColor(TAB_BORDER) || theme.getColor(contrastBorder)); - if (borderColor) { - collector.addRule(` - .monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container { - border-bottom: 1px solid ${borderColor}; - } - `); - } - } - // Add bottom border to tabs when wrapping const borderColor = theme.getColor(TAB_BORDER); if (borderColor) { diff --git a/src/vs/workbench/browser/parts/notifications/notificationsActions.ts b/src/vs/workbench/browser/parts/notifications/notificationsActions.ts index 3b0d34ea4af..3402b0d5987 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsActions.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsActions.ts @@ -154,10 +154,12 @@ interface NotificationActionMetrics { } type NotificationActionMetricsClassification = { - id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'bpasero'; comment: 'The identifier of the action that was run from a notification.' }; - actionLabel: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'bpasero'; comment: 'The label of the action that was run from a notification.' }; - source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'bpasero'; comment: 'The source of the notification where an action was run.' }; - silent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'bpasero'; comment: 'Whether the notification where an action was run is silent or not.' }; + id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The identifier of the action that was run from a notification.' }; + actionLabel: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The label of the action that was run from a notification.' }; + source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The source of the notification where an action was run.' }; + silent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the notification where an action was run is silent or not.' }; + owner: 'bpasero'; + comment: 'Tracks when actions are fired from notifcations and how they were fired.'; }; export class NotificationActionRunner extends ActionRunner { diff --git a/src/vs/workbench/browser/parts/notifications/notificationsList.ts b/src/vs/workbench/browser/parts/notifications/notificationsList.ts index 58e32d35e78..9989466693c 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsList.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsList.ts @@ -301,10 +301,9 @@ registerThemingParticipant((theme, collector) => { // Notification Error Icon const notificationErrorIconForegroundColor = theme.getColor(NOTIFICATIONS_ERROR_ICON_FOREGROUND); if (notificationErrorIconForegroundColor) { - const errorCodiconSelector = Codicon.error.cssSelector; collector.addRule(` - .monaco-workbench .notifications-center ${errorCodiconSelector}, - .monaco-workbench .notifications-toasts ${errorCodiconSelector} { + .monaco-workbench .notifications-center ${Codicon.error.cssSelector}, + .monaco-workbench .notifications-toasts ${Codicon.error.cssSelector} { color: ${notificationErrorIconForegroundColor}; }`); } @@ -312,10 +311,9 @@ registerThemingParticipant((theme, collector) => { // Notification Warning Icon const notificationWarningIconForegroundColor = theme.getColor(NOTIFICATIONS_WARNING_ICON_FOREGROUND); if (notificationWarningIconForegroundColor) { - const warningCodiconSelector = Codicon.warning.cssSelector; collector.addRule(` - .monaco-workbench .notifications-center ${warningCodiconSelector}, - .monaco-workbench .notifications-toasts ${warningCodiconSelector} { + .monaco-workbench .notifications-center ${Codicon.warning.cssSelector}, + .monaco-workbench .notifications-toasts ${Codicon.warning.cssSelector} { color: ${notificationWarningIconForegroundColor}; }`); } @@ -323,10 +321,9 @@ registerThemingParticipant((theme, collector) => { // Notification Info Icon const notificationInfoIconForegroundColor = theme.getColor(NOTIFICATIONS_INFO_ICON_FOREGROUND); if (notificationInfoIconForegroundColor) { - const infoCodiconSelector = Codicon.info.cssSelector; collector.addRule(` - .monaco-workbench .notifications-center ${infoCodiconSelector}, - .monaco-workbench .notifications-toasts ${infoCodiconSelector} { + .monaco-workbench .notifications-center ${Codicon.info.cssSelector}, + .monaco-workbench .notifications-toasts ${Codicon.info.cssSelector} { color: ${notificationInfoIconForegroundColor}; }`); } diff --git a/src/vs/workbench/browser/parts/notifications/notificationsTelemetry.ts b/src/vs/workbench/browser/parts/notifications/notificationsTelemetry.ts index b498207420e..4d6e3823333 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsTelemetry.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsTelemetry.ts @@ -16,9 +16,11 @@ export interface NotificationMetrics { } export type NotificationMetricsClassification = { - id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'bpasero'; comment: 'The identifier of the source of the notification.' }; - silent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'bpasero'; comment: 'Whethe the notification is silent or not.' }; - source?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'bpasero'; comment: 'The source of the notification.' }; + id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The identifier of the source of the notification.' }; + silent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the notification is silent or not.' }; + source?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The source of the notification.' }; + owner: 'bpasero'; + comment: 'Helps us gain insights to what notifications are being shown, how many, and if they are silent or not.'; }; export function notificationToMetrics(message: NotificationMessage, source: string | undefined, silent: boolean): NotificationMetrics { diff --git a/src/vs/workbench/browser/parts/panel/panelPart.ts b/src/vs/workbench/browser/parts/panel/panelPart.ts index 3e87cd1f5fd..ec28dd8769c 100644 --- a/src/vs/workbench/browser/parts/panel/panelPart.ts +++ b/src/vs/workbench/browser/parts/panel/panelPart.ts @@ -45,6 +45,7 @@ import { IPaneCompositePart, IPaneCompositeSelectorPart } from 'vs/workbench/bro import { IPartOptions } from 'vs/workbench/browser/part'; import { StringSHA1 } from 'vs/base/common/hash'; import { URI } from 'vs/base/common/uri'; +import { Extensions, IProfileStorageRegistry } from 'vs/workbench/services/profiles/common/profileStorageRegistry'; interface ICachedPanel { id: string; @@ -208,6 +209,12 @@ export abstract class BasePanelPart extends CompositePart<PaneComposite> impleme // Global Panel Actions this.globalActions = this._register(this.instantiationService.createInstance(CompositeMenuActions, partId === Parts.PANEL_PART ? MenuId.PanelTitle : MenuId.AuxiliaryBarTitle, undefined, undefined)); this._register(this.globalActions.onDidChange(() => this.updateGlobalToolbarActions())); + + Registry.as<IProfileStorageRegistry>(Extensions.ProfileStorageRegistry) + .registerKeys([{ + key: this.pinnedPanelsKey, + description: localize('pinned view containers', "Panel entries visibility customizations") + }]); } protected abstract getActivityHoverOptions(): IActivityHoverOptions; @@ -370,6 +377,14 @@ export abstract class BasePanelPart extends CompositePart<PaneComposite> impleme if (viewContainerModel.activeViewDescriptors.length) { contextKey.set(true); this.compositeBar.addComposite({ id: viewContainer.id, name: viewContainer.title, order: viewContainer.order, requestedIndex: viewContainer.requestedIndex }); + + const activeComposite = this.getActiveComposite(); + if (activeComposite === undefined || activeComposite.getId() === viewContainer.id) { + this.compositeBar.activateComposite(viewContainer.id); + } + + this.layoutCompositeBar(); + this.layoutEmptyMessage(); } else if (viewContainer.hideIfEmpty) { contextKey.set(false); this.hideComposite(viewContainer.id); diff --git a/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css b/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css index 8a387c00f71..8363eed17f6 100644 --- a/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css +++ b/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css @@ -127,7 +127,7 @@ } .monaco-workbench .part.statusbar > .items-container > .statusbar-item.compact-right > a { - padding: 0 3px 0 5px; + padding: 0 3px; } .monaco-workbench .part.statusbar > .items-container > .statusbar-item > a:hover:not(.disabled) { diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts b/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts index 09f69549739..f3ea7a8e5f6 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts @@ -8,6 +8,9 @@ import { isStatusbarEntryLocation, IStatusbarEntryLocation, StatusbarAlignment } import { hide, show, isAncestor } from 'vs/base/browser/dom'; import { IStorageService, StorageScope, IStorageValueChangeEvent, StorageTarget } from 'vs/platform/storage/common/storage'; import { Emitter } from 'vs/base/common/event'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { Extensions, IProfileStorageRegistry } from 'vs/workbench/services/profiles/common/profileStorageRegistry'; +import { localize } from 'vs/nls'; export interface IStatusbarEntryPriority { @@ -64,6 +67,12 @@ export class StatusbarViewModel extends Disposable { this.restoreState(); this.registerListeners(); + + Registry.as<IProfileStorageRegistry>(Extensions.ProfileStorageRegistry) + .registerKeys([{ + key: StatusbarViewModel.HIDDEN_ENTRIES_KEY, + description: localize('statusbar.hidden', "Status bar entries visibility customizations"), + }]); } private restoreState(): void { diff --git a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css index 2471bcee7f8..088a77c1acd 100644 --- a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css +++ b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css @@ -3,27 +3,51 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-workbench .part.titlebar > .titlebar-container { +/* Part Element */ +.monaco-workbench .part.titlebar { + display: flex; + flex-direction: row; +} + +.monaco-workbench.mac .part.titlebar { + flex-direction: row-reverse; +} + +/* Root Container */ +.monaco-workbench .part.titlebar>.titlebar-container { box-sizing: border-box; - width: 100%; - padding: 0 70px; overflow: hidden; - flex-shrink: 0; + flex-shrink: 1; + flex-grow: 1; align-items: center; justify-content: center; user-select: none; -webkit-user-select: none; - zoom: 1; /* prevent zooming */ - line-height: 22px; - height: 22px; display: flex; + height: 100%; + width: 100%; } -.monaco-workbench .part.titlebar > .titlebar-container { - transform-origin: 0 0; +/* Account for zooming */ +.monaco-workbench .part.titlebar>.titlebar-container.counter-zoom { + zoom: calc(1.0 / var(--zoom-factor)); } -.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-drag-region { +/* Platform specific root element */ +.monaco-workbench.mac .part.titlebar>.titlebar-container { + line-height: 22px; +} + +.monaco-workbench.web .part.titlebar>.titlebar-container, +.monaco-workbench.windows .part.titlebar>.titlebar-container, +.monaco-workbench.linux .part.titlebar>.titlebar-container { + height: 30px; + line-height: 30px; + justify-content: left; +} + +/* Draggable region */ +.monaco-workbench .part.titlebar>.titlebar-container>.titlebar-drag-region { top: 0; left: 0; display: block; @@ -33,7 +57,43 @@ -webkit-app-region: drag; } -.monaco-workbench .part.titlebar > .titlebar-container > .window-title { +/* Command Center */ +.monaco-workbench .part.titlebar>.titlebar-container>.title-menu .action-item.quickopen { + color: var(--vscode-input-foreground); + border: 1px solid var(--vscode-dropdown-border); + height: 20px; + line-height: 20px; + width: 38vw; + max-width: 600px; + margin: 4px 4px; + flex-direction: row; + justify-content: center; + overflow: hidden; +} + +.monaco-workbench .part.titlebar>.titlebar-container>.title-menu .action-item.quickopen:HOVER, +.monaco-workbench .part.titlebar>.titlebar-container>.title-menu .action-item.quickopen .action-container .action-label:HOVER { + background-color: var(--vscode-dropdown-border); + line-height: 18px; +} + +.monaco-workbench .part.titlebar>.titlebar-container>.title-menu .action-item.quickopen .action-container { + flex: 1 0 auto; + display: flex; + justify-content: center; +} + +.monaco-workbench .part.titlebar>.titlebar-container>.title-menu .action-item.quickopen .action-container .keybinding { + font-size: 11px; + padding: 3px; +} + +.monaco-workbench .part.titlebar>.titlebar-container>.title-menu .action-item.quickopen .dropdown-action-container { + margin-left: auto; +} + +/* Window title text */ +.monaco-workbench .part.titlebar>.titlebar-container>.window-title { flex: 0 1 auto; font-size: 12px; overflow: hidden; @@ -41,13 +101,12 @@ text-overflow: ellipsis; margin-left: auto; margin-right: auto; - zoom: 1; /* prevent zooming */ } /* Windows/Linux: Rules for custom title (icon, window controls) */ -.monaco-workbench.web .part.titlebar > .titlebar-container, -.monaco-workbench.windows .part.titlebar > .titlebar-container, -.monaco-workbench.linux .part.titlebar > .titlebar-container { +.monaco-workbench.web .part.titlebar>.titlebar-container, +.monaco-workbench.windows .part.titlebar>.titlebar-container, +.monaco-workbench.linux .part.titlebar>.titlebar-container { padding: 0; height: 30px; line-height: 30px; @@ -55,23 +114,31 @@ overflow: visible; } -.monaco-workbench.web .part.titlebar > .titlebar-container > .window-title, -.monaco-workbench.windows .part.titlebar > .titlebar-container > .window-title, -.monaco-workbench.linux .part.titlebar > .titlebar-container > .window-title { +.monaco-workbench.web .part.titlebar>.titlebar-container>.window-title, +.monaco-workbench.windows .part.titlebar>.titlebar-container>.window-title, +.monaco-workbench.linux .part.titlebar>.titlebar-container>.window-title { cursor: default; } -.monaco-workbench .part.titlebar > .titlebar-container > .menubar { - /* move menubar above drag region as negative z-index on drag region cause greyscale AA */ - z-index: 2500; +.monaco-workbench .part.titlebar>.titlebar-container.enable-title-menu>.window-title { + display: none; } -.monaco-workbench.linux .part.titlebar > .titlebar-container > .window-title { +.monaco-workbench.linux .part.titlebar>.titlebar-container>.window-title { font-size: inherit; + /* see #55435 */ +} + +/* Menubar */ +.monaco-workbench .part.titlebar>.titlebar-container>.menubar { + /* move menubar above drag region as negative z-index on drag region cause greyscale AA */ + z-index: 2500; + min-width: 36px; } -.monaco-workbench.windows .part.titlebar > .titlebar-container > .resizer, -.monaco-workbench.linux .part.titlebar > .titlebar-container > .resizer { +/* Resizer */ +.monaco-workbench.windows .part.titlebar>.titlebar-container>.resizer, +.monaco-workbench.linux .part.titlebar>.titlebar-container>.resizer { -webkit-app-region: no-drag; position: absolute; top: 0; @@ -79,12 +146,13 @@ height: 4px; } -.monaco-workbench.windows.fullscreen .part.titlebar > .titlebar-container > .resizer, -.monaco-workbench.linux.fullscreen .part.titlebar > .titlebar-container > .resizer { +.monaco-workbench.windows.fullscreen .part.titlebar>.titlebar-container>.resizer, +.monaco-workbench.linux.fullscreen .part.titlebar>.titlebar-container>.resizer { display: none; } -.monaco-workbench .part.titlebar > .titlebar-container > .window-appicon { +/* App Icon */ +.monaco-workbench .part.titlebar>.titlebar-container>.window-appicon { width: 35px; height: 100%; position: relative; @@ -92,20 +160,29 @@ flex-shrink: 0; } -.monaco-workbench .part.titlebar > .titlebar-container > .window-appicon:not(.codicon) { +.monaco-workbench .part.titlebar>.titlebar-container>.window-appicon:not(.codicon) { background-image: url('../../../media/code-icon.svg'); background-repeat: no-repeat; background-position: center center; background-size: 16px; } -.monaco-workbench .part.titlebar > .titlebar-container .window-appicon > .home-bar-icon-badge { +.monaco-workbench .part.titlebar>.titlebar-container>.window-appicon.codicon { + line-height: 30px; +} + +.monaco-workbench.fullscreen .part.titlebar>.titlebar-container>.window-appicon { + display: none; +} + +.monaco-workbench .part.titlebar>.titlebar-container .window-appicon>.home-bar-icon-badge { position: absolute; right: 9px; bottom: 6px; width: 8px; height: 8px; - z-index: 1; /* on top of home indicator */ + z-index: 1; + /* on top of home indicator */ background-image: url('../../../media/code-icon.svg'); background-repeat: no-repeat; background-position: center center; @@ -115,71 +192,32 @@ border-left: 1px solid transparent; } -.monaco-workbench .part.titlebar > .titlebar-container > .window-appicon.codicon { - line-height: 30px; -} - -.monaco-workbench.fullscreen .part.titlebar > .titlebar-container > .window-appicon { - display: none; -} - -.monaco-workbench .part.titlebar > .titlebar-container > .window-controls-container { +/* Window Controls (Minimize, Max/Restore, Close) */ +.monaco-workbench .part.titlebar>.window-controls-container { display: flex; flex-grow: 0; flex-shrink: 0; text-align: center; - position: relative; z-index: 3000; -webkit-app-region: no-drag; - height: 100%; - min-width: 138px; - margin-left: auto; -} - -.monaco-workbench .part.titlebar > .titlebar-container > .window-controls-container.show-layout-control { - min-width: 160px; -} - -.monaco-workbench.web .part.titlebar > .titlebar-container > .window-controls-container.show-layout-control { - min-width: 28px; - padding-right: 8px; -} - -.monaco-workbench.mac:not(.web) .part.titlebar > .titlebar-container > .window-controls-container { - position: absolute; - right: 8px; - min-width: 28px; - display: none; + height: 30px; + width: 138px; + zoom: calc(1 / var(--zoom-factor)); } -.monaco-workbench.mac:not(.web) .part.titlebar > .titlebar-container > .window-controls-container.show-layout-control { - display: flex; +.monaco-workbench.mac .part.titlebar>.window-controls-container { + width: 70px; + height: env(titlebar-area-width, 28px); } -.monaco-workbench.fullscreen .part.titlebar > .titlebar-container > .window-controls-container { +.monaco-workbench.web .part.titlebar>.window-controls-container, +.monaco-workbench.fullscreen .part.titlebar>.window-controls-container { display: none; background-color: transparent; } -.monaco-workbench .part.titlebar > .titlebar-container > .window-controls-container > .layout-dropdown-container { - padding-right: 2px; - display: none; -} - -.monaco-workbench .part.titlebar > .titlebar-container > .window-controls-container.show-layout-control > .layout-dropdown-container { - display: flex; - justify-content: center; -} - -.monaco-workbench:not(.mac):not(.web) .part.titlebar > .titlebar-container > .window-controls-container.show-layout-control > .layout-dropdown-container { - min-width: 46px; -} - -.monaco-workbench .part.titlebar > .titlebar-container > .window-controls-container.show-layout-control > .layout-dropdown-container .codicon { - color: inherit; -} - -.monaco-workbench .part.titlebar > .titlebar-container > .window-controls-container > .window-icon { +/* Window Control Icons */ +.monaco-workbench .part.titlebar>.window-controls-container>.window-icon { display: inline-block; line-height: 30px; height: 100%; @@ -187,18 +225,47 @@ font-size: 16px; } -.monaco-workbench .part.titlebar > .titlebar-container > .window-controls-container > .window-icon:hover { +.monaco-workbench .part.titlebar>.window-controls-container>.window-icon:hover { background-color: rgba(255, 255, 255, 0.1); } -.monaco-workbench .part.titlebar.light > .titlebar-container > .window-controls-container > .window-icon:hover { +.monaco-workbench .part.titlebar.light>.window-controls-container>.window-icon:hover { background-color: rgba(0, 0, 0, 0.1); } -.monaco-workbench .part.titlebar > .titlebar-container > .window-controls-container > .window-icon.window-close:hover { +.monaco-workbench .part.titlebar>.window-controls-container>.window-icon.window-close:hover { background-color: rgba(232, 17, 35, 0.9); } -.monaco-workbench .part.titlebar > .titlebar-container > .window-controls-container .window-icon.window-close:hover { +.monaco-workbench .part.titlebar>.window-controls-container .window-icon.window-close:hover { color: white; } + +/* Layout Controls */ +.monaco-workbench .part.titlebar>.titlebar-container>.layout-controls-container { + display: none; + padding-right: 2px; + flex-grow: 0; + flex-shrink: 0; + text-align: center; + position: relative; + z-index: 3000; + -webkit-app-region: no-drag; + height: 100%; + margin-left: auto; + min-width: 28px; +} + +.monaco-workbench.mac:not(.web) .part.titlebar>.layout-controls-container { + position: absolute; + right: 8px; +} + +.monaco-workbench .part.titlebar>.titlebar-container>.layout-controls-container.show-layout-control { + display: flex; + justify-content: center; +} + +.monaco-workbench .part.titlebar>.titlebar-container>.layout-controls-container .codicon { + color: inherit; +} diff --git a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts index fd8c91724bf..88e69dc8ceb 100644 --- a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts +++ b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts @@ -921,10 +921,6 @@ export class CustomMenubarControl extends MenubarControl { } layout(dimension: Dimension) { - if (this.container) { - this.container.style.height = `${dimension.height}px`; - } - this.menubar?.update(this.getMenuBarOptions()); } diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index 4934df5077c..2dbbcce47c8 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -12,10 +12,10 @@ import { getZoomFactor } from 'vs/base/browser/browser'; import { MenuBarVisibility, getTitleBarStyle, getMenuBarVisibility } from 'vs/platform/window/common/window'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; -import { IAction } from 'vs/base/common/actions'; +import { IAction, toAction } from 'vs/base/common/actions'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { DisposableStore, dispose } from 'vs/base/common/lifecycle'; +import { DisposableStore, dispose, toDisposable } from 'vs/base/common/lifecycle'; import { EditorResourceAccessor, Verbosity, SideBySideEditor } from 'vs/workbench/common/editor'; import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; @@ -25,7 +25,7 @@ import { isMacintosh, isWindows, isLinux, isWeb } from 'vs/base/common/platform' import { URI } from 'vs/base/common/uri'; import { Color } from 'vs/base/common/color'; import { trim } from 'vs/base/common/strings'; -import { EventType, EventHelper, Dimension, isAncestor, append, $, addDisposableListener, runAtThisOrScheduleAtNextAnimationFrame, prepend } from 'vs/base/browser/dom'; +import { EventType, EventHelper, Dimension, isAncestor, append, $, addDisposableListener, runAtThisOrScheduleAtNextAnimationFrame, prepend, clearNode } from 'vs/base/browser/dom'; import { CustomMenubarControl } from 'vs/workbench/browser/parts/titlebar/menubarControl'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { template } from 'vs/base/common/labels'; @@ -34,8 +34,8 @@ import { Emitter } from 'vs/base/common/event'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { Parts, IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { RunOnceScheduler } from 'vs/base/common/async'; -import { createActionViewItem, createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { IMenuService, IMenu, MenuId } from 'vs/platform/actions/common/actions'; +import { createActionViewItem, createAndFillInContextMenuActions, DropdownWithDefaultActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IMenuService, IMenu, MenuId, SubmenuItemAction, MenuRegistry } from 'vs/platform/actions/common/actions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IProductService } from 'vs/platform/product/common/productService'; @@ -45,6 +45,7 @@ import { Codicon } from 'vs/base/common/codicons'; import { getVirtualWorkspaceLocation } from 'vs/platform/workspace/common/virtualWorkspace'; import { getIconRegistry } from 'vs/platform/theme/common/iconRegistry'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; export class TitlebarPart extends Part implements ITitleService { @@ -57,7 +58,7 @@ export class TitlebarPart extends Part implements ITitleService { readonly minimumWidth: number = 0; readonly maximumWidth: number = Number.POSITIVE_INFINITY; - get minimumHeight(): number { return 30 / (this.currentMenubarVisibility === 'hidden' ? getZoomFactor() : 1); } + get minimumHeight(): number { return 30 / (this.currentMenubarVisibility === 'hidden' || getZoomFactor() < 1 ? getZoomFactor() : 1); } get maximumHeight(): number { return this.minimumHeight; } //#endregion @@ -68,15 +69,19 @@ export class TitlebarPart extends Part implements ITitleService { declare readonly _serviceBrand: undefined; protected rootContainer!: HTMLElement; + protected windowControls: HTMLElement | undefined; + protected readonly titleMenuElement: HTMLElement = $('div.title-menu'); protected title!: HTMLElement; protected customMenubar: CustomMenubarControl | undefined; protected appIcon: HTMLElement | undefined; private appIconBadge: HTMLElement | undefined; protected menubar?: HTMLElement; - protected windowControls: HTMLElement | undefined; + protected layoutControls: HTMLElement | undefined; private layoutToolbar: ToolBar | undefined; protected lastLayoutDimensions: Dimension | undefined; + + private readonly titleMenuDisposables = this._register(new DisposableStore()); private titleBarStyle: 'native' | 'custom'; private pendingTitle: string | undefined; @@ -87,6 +92,7 @@ export class TitlebarPart extends Part implements ITitleService { private readonly activeEditorListeners = this._register(new DisposableStore()); private readonly titleUpdater = this._register(new RunOnceScheduler(() => this.doUpdateTitle(), 0)); + private readonly onDidUpdateTitle = new Emitter<void>(); private contextMenu: IMenu; @@ -105,6 +111,7 @@ export class TitlebarPart extends Part implements ITitleService { @IContextKeyService private readonly contextKeyService: IContextKeyService, @IHostService private readonly hostService: IHostService, @IProductService private readonly productService: IProductService, + @IKeybindingService private readonly keybindingService: IKeybindingService, ) { super(Parts.TITLEBAR_PART, { hasTitle: false }, themeService, storageService, layoutService); @@ -150,8 +157,12 @@ export class TitlebarPart extends Part implements ITitleService { } } - if (this.titleBarStyle !== 'native' && this.windowControls && event.affectsConfiguration('workbench.layoutControl.enabled')) { - this.windowControls.classList.toggle('show-layout-control', this.layoutControlEnabled); + if (this.titleBarStyle !== 'native' && this.layoutControls && event.affectsConfiguration('workbench.layoutControl.enabled')) { + this.layoutControls.classList.toggle('show-layout-control', this.layoutControlEnabled); + } + + if (event.affectsConfiguration('window.experimental.titleMenu')) { + this.updateTitleMenu(); } } @@ -196,11 +207,11 @@ export class TitlebarPart extends Part implements ITitleService { this.pendingTitle = title; } - if ((isWeb || isWindows || isLinux) && this.title) { - if (this.lastLayoutDimensions) { - this.updateLayout(this.lastLayoutDimensions); - } + if (this.lastLayoutDimensions) { + this.updateLayout(this.lastLayoutDimensions); } + + this.onDidUpdateTitle.fire(); } private getWindowTitle(): string { @@ -356,7 +367,7 @@ export class TitlebarPart extends Part implements ITitleService { this.customMenubar = this._register(this.instantiationService.createInstance(CustomMenubarControl)); - this.menubar = this.rootContainer.insertBefore($('div.menubar'), this.title); + this.menubar = this.rootContainer.insertBefore($('div.menubar'), this.titleMenuElement); this.menubar.setAttribute('role', 'menubar'); this.customMenubar.create(this.menubar); @@ -364,9 +375,54 @@ export class TitlebarPart extends Part implements ITitleService { this._register(this.customMenubar.onVisibilityChange(e => this.onMenubarVisibilityChanged(e))); } + private updateTitleMenu(): void { + this.titleMenuDisposables.clear(); + const enableTitleMenu = this.configurationService.getValue<boolean>('window.experimental.titleMenu'); + this.rootContainer.classList.toggle('enable-title-menu', enableTitleMenu); + if (!enableTitleMenu) { + return; + } + + + const that = this; + const titleToolbar = new ToolBar(this.titleMenuElement, this.contextMenuService, { + actionViewItemProvider: (action) => { + + if (action instanceof SubmenuItemAction && action.item.submenu === MenuId.TitleMenuQuickPick) { + class QuickInputDropDown extends DropdownWithDefaultActionViewItem { + override render(container: HTMLElement): void { + super.render(container); + container.classList.add('quickopen'); + container.title = that.getWindowTitle(); + this._store.add(that.onDidUpdateTitle.event(() => container.title = that.getWindowTitle())); + } + } + return that.instantiationService.createInstance(QuickInputDropDown, action, { + keybindingProvider: action => that.keybindingService.lookupKeybinding(action.id), + renderKeybindingWithDefaultActionLabel: true + }); + } + return undefined; + } + }); + const titleMenu = this.titleMenuDisposables.add(this.menuService.createMenu(MenuId.TitleMenu, this.contextKeyService)); + const titleMenuDisposables = this.titleMenuDisposables.add(new DisposableStore()); + const updateTitleMenu = () => { + titleMenuDisposables.clear(); + const actions: IAction[] = []; + titleMenuDisposables.add(createAndFillInContextMenuActions(titleMenu, undefined, actions)); + titleToolbar.setActions(actions); + }; + this.titleMenuDisposables.add(titleMenu.onDidChange(updateTitleMenu)); + this.titleMenuDisposables.add(this.keybindingService.onDidUpdateKeybindings(updateTitleMenu)); + this.titleMenuDisposables.add(toDisposable(() => clearNode(this.titleMenuElement))); + updateTitleMenu(); + } + override createContentArea(parent: HTMLElement): HTMLElement { this.element = parent; this.rootContainer = append(parent, $('.titlebar-container')); + append(this.rootContainer, this.titleMenuElement); // App Icon (Native Windows/Linux and Web) if (!isMacintosh || isWeb) { @@ -395,6 +451,9 @@ export class TitlebarPart extends Part implements ITitleService { this.installMenubar(); } + // Title Menu + this.updateTitleMenu(); + // Title this.title = append(this.rootContainer, $('div.window-title')); if (this.pendingTitle) { @@ -404,16 +463,22 @@ export class TitlebarPart extends Part implements ITitleService { } if (this.titleBarStyle !== 'native') { - this.windowControls = append(this.rootContainer, $('div.window-controls-container')); - this.windowControls.classList.toggle('show-layout-control', this.layoutControlEnabled); + this.layoutControls = append(this.rootContainer, $('div.layout-controls-container')); + this.layoutControls.classList.toggle('show-layout-control', this.layoutControlEnabled); - const layoutDropdownContainer = append(this.windowControls, $('div.layout-dropdown-container')); - this.layoutToolbar = new ToolBar(layoutDropdownContainer, this.contextMenuService, { + this.layoutToolbar = new ToolBar(this.layoutControls, this.contextMenuService, { actionViewItemProvider: action => { return createActionViewItem(this.instantiationService, action); - } + }, + allowContextMenu: true }); + this._register(addDisposableListener(this.layoutControls, EventType.CONTEXT_MENU, e => { + EventHelper.stop(e); + + this.onLayoutControlContextMenu(e, this.layoutControls!); + })); + const menu = this._register(this.menuService.createMenu(MenuId.LayoutControlMenu, this.contextKeyService)); const updateLayoutMenu = () => { @@ -433,6 +498,8 @@ export class TitlebarPart extends Part implements ITitleService { updateLayoutMenu(); } + this.windowControls = append(this.element, $('div.window-controls-container')); + // Context menu on title [EventType.CONTEXT_MENU, EventType.MOUSE_DOWN].forEach(event => { this._register(addDisposableListener(this.title, event, e => { @@ -455,6 +522,10 @@ export class TitlebarPart extends Part implements ITitleService { return; } + if (e.target && isAncestor(e.target as HTMLElement, this.titleMenuElement)) { + return; + } + const active = document.activeElement; setTimeout(() => { if (active instanceof HTMLElement) { @@ -507,7 +578,6 @@ export class TitlebarPart extends Part implements ITitleService { } private onContextMenu(e: MouseEvent): void { - // Find target anchor const event = new StandardMouseEvent(e); const anchor = { x: event.posx, y: event.posy }; @@ -524,20 +594,41 @@ export class TitlebarPart extends Part implements ITitleService { }); } - protected adjustTitleMarginToCenter(): void { - if (this.customMenubar && this.menubar) { - const leftMarker = (this.appIcon ? this.appIcon.clientWidth : 0) + this.menubar.clientWidth + 10; - const rightMarker = this.element.clientWidth - 10; - - // Not enough space to center the titlebar within window, - // Center between menu and window controls - if (leftMarker > (this.element.clientWidth - this.title.clientWidth) / 2 || - rightMarker < (this.element.clientWidth + this.title.clientWidth) / 2) { - this.title.style.position = ''; - this.title.style.left = ''; - this.title.style.transform = ''; - return; + private onLayoutControlContextMenu(e: MouseEvent, el: HTMLElement): void { + // Find target anchor + const event = new StandardMouseEvent(e); + const anchor = { x: event.posx, y: event.posy }; + + const actions: IAction[] = []; + actions.push(toAction({ + id: 'layoutControl.hide', + label: localize('layoutControl.hide', "Hide Layout Control"), + run: () => { + this.configurationService.updateValue('workbench.layoutControl.enabled', false); } + })); + + // Show it + this.contextMenuService.showContextMenu({ + getAnchor: () => anchor, + getActions: () => actions, + domForShadowRoot: el + }); + } + + protected adjustTitleMarginToCenter(): void { + const base = isMacintosh ? (this.windowControls?.clientWidth ?? 0) : 0; + const leftMarker = base + (this.appIcon?.clientWidth ?? 0) + (this.menubar?.clientWidth ?? 0) + 10; + const rightMarker = base + this.rootContainer.clientWidth - (this.layoutControls?.clientWidth ?? 0) - 10; + + // Not enough space to center the titlebar within window, + // Center between left and right controls + if (leftMarker > (this.rootContainer.clientWidth + (this.windowControls?.clientWidth ?? 0) - this.title.clientWidth) / 2 || + rightMarker < (this.rootContainer.clientWidth + (this.windowControls?.clientWidth ?? 0) + this.title.clientWidth) / 2) { + this.title.style.position = ''; + this.title.style.left = ''; + this.title.style.transform = ''; + return; } this.title.style.position = 'absolute'; @@ -557,16 +648,13 @@ export class TitlebarPart extends Part implements ITitleService { this.lastLayoutDimensions = dimension; if (getTitleBarStyle(this.configurationService) === 'custom') { - // Only prevent zooming behavior on macOS or when the menubar is not visible - if ((!isWeb && isMacintosh) || this.currentMenubarVisibility === 'hidden') { - this.rootContainer.style.height = `${100.0 * getZoomFactor()}%`; - this.rootContainer.style.width = `${100.0 * getZoomFactor()}%`; - this.rootContainer.style.transform = `scale(${1 / getZoomFactor()})`; - } else { - this.rootContainer.style.height = '100%'; - this.rootContainer.style.width = '100%'; - this.rootContainer.style.transform = ''; - } + // Prevent zooming behavior if any of the following conditions are met: + // 1. Native macOS + // 2. Menubar is hidden + // 3. Shrinking below the window control size (zoom < 1) + const zoomFactor = getZoomFactor(); + this.element.style.setProperty('--zoom-factor', zoomFactor.toString()); + this.rootContainer.classList.toggle('counter-zoom', zoomFactor < 1 || (!isWeb && isMacintosh) || this.currentMenubarVisibility === 'hidden'); runAtThisOrScheduleAtNextAnimationFrame(() => this.adjustTitleMarginToCenter()); @@ -609,3 +697,9 @@ registerThemingParticipant((theme, collector) => { `); } }); + +MenuRegistry.appendMenuItem(MenuId.TitleMenu, { + submenu: MenuId.TitleMenuQuickPick, + title: localize('title', "Select Mode"), + order: Number.MAX_SAFE_INTEGER +}); diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index 91df2786f62..e8f931902e7 100644 --- a/src/vs/workbench/browser/parts/views/treeView.ts +++ b/src/vs/workbench/browser/parts/views/treeView.ts @@ -65,6 +65,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { Mimes } from 'vs/base/common/mime'; import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; import { IDataTransfer } from 'vs/workbench/common/dnd'; +import { ThemeSettings } from 'vs/workbench/services/themes/common/workbenchThemeService'; export class TreeViewPane extends ViewPane { @@ -967,12 +968,14 @@ class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyS if (!(node instanceof ResolvableTreeItem) || !node.hasResolve) { if (resource && !node.tooltip) { return undefined; - } else if (!node.tooltip) { + } else if (node.tooltip === undefined) { return label; } else if (!isString(node.tooltip)) { return { markdown: node.tooltip, markdownNotSupportedFallback: resource ? undefined : renderMarkdownAsPlaintext(node.tooltip) }; // Passing undefined as the fallback for a resource falls back to the old native hover - } else { + } else if (node.tooltip !== '') { return node.tooltip; + } else { + return undefined; } } @@ -1018,13 +1021,13 @@ class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyS templateData.actionBar.clear(); templateData.icon.style.color = ''; - if (resource || this.isFileKindThemeIcon(node.themeIcon)) { + if (resource) { const fileDecorations = this.configurationService.getValue<{ colors: boolean; badges: boolean }>('explorer.decorations'); const labelResource = resource ? resource : URI.parse('missing:_icon_resource'); templateData.resourceLabel.setResource({ name: label, description, resource: labelResource }, { fileKind: this.getFileKind(node), title, - hideIcon: !!iconUrl || !!node.themeIcon, + hideIcon: !!iconUrl || this.shouldShowThemeIcon(!!resource, node.themeIcon), fileDecorations, extraClasses: ['custom-view-tree-node-item-resourceLabel'], matches: matches ? matches : createMatches(element.filterData), @@ -1047,7 +1050,7 @@ class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyS let iconClass: string | undefined; // If there is a resource for this tree item then we should respect the file icon theme's choice about // whether to show a folder icon. - if (node.themeIcon && (!resource || !this.isFolderThemeIcon(node.themeIcon) || this.themeService.getFileIconTheme().hasFolderIcons)) { + if (this.shouldShowThemeIcon(!!resource, node.themeIcon)) { iconClass = ThemeIcon.asClassName(node.themeIcon); if (node.themeIcon.color) { templateData.icon.style.color = this.themeService.getColorTheme().getColor(node.themeIcon.color.id)?.toString() ?? ''; @@ -1080,6 +1083,27 @@ class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyS container.parentElement!.classList.toggle('align-icon-with-twisty', this.aligner.alignIconWithTwisty(treeItem)); } + private shouldShowThemeIcon(hasResource: boolean, icon: ThemeIcon | undefined): icon is ThemeIcon { + if (!icon) { + return false; + } + + if (hasResource && (this.isFileKindThemeIcon(icon) || !this.shouldShowFileIcons())) { + return false; + } else if (hasResource && (this.isFolderThemeIcon(icon) || !this.shouldShowFolderIcons())) { + return false; + } + return true; + } + + private shouldShowFileIcons(): boolean { + return this.configurationService.getValue(ThemeSettings.FILE_ICON_THEME); + } + + private shouldShowFolderIcons(): boolean { + return this.themeService.getFileIconTheme().hasFolderIcons && this.shouldShowFileIcons(); + } + private isFolderThemeIcon(icon: ThemeIcon | undefined): boolean { return icon?.id === FolderThemeIcon.id; } @@ -1253,6 +1277,7 @@ export class CustomTreeView extends AbstractTreeView { constructor( id: string, title: string, + private readonly extensionId: string, @IThemeService themeService: IThemeService, @IInstantiationService instantiationService: IInstantiationService, @ICommandService commandService: ICommandService, @@ -1265,13 +1290,28 @@ export class CustomTreeView extends AbstractTreeView { @IContextKeyService contextKeyService: IContextKeyService, @IHoverService hoverService: IHoverService, @IExtensionService private readonly extensionService: IExtensionService, - @IActivityService activityService: IActivityService + @IActivityService activityService: IActivityService, + @ITelemetryService private readonly telemetryService: ITelemetryService ) { super(id, title, themeService, instantiationService, commandService, configurationService, progressService, contextMenuService, keybindingService, notificationService, viewDescriptorService, hoverService, contextKeyService, activityService); } protected activate() { if (!this.activated) { + type ExtensionViewTelemetry = { + extensionId: string; + id: string; + }; + type ExtensionViewTelemetryMeta = { + extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Id of the extension' }; + id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Id of the view' }; + owner: 'digitarald'; + comment: 'Helps to gain insights on what extension contributed views are most popular'; + }; + this.telemetryService.publicLog2<ExtensionViewTelemetry, ExtensionViewTelemetryMeta>('Extension:ViewActivate', { + extensionId: this.extensionId, + id: this.id, + }); this.createTree(); this.progressService.withProgress({ location: this.id }, () => this.extensionService.activateByEvent(`onView:${this.id}`)) .then(() => timeout(2000)) @@ -1391,13 +1431,7 @@ export class CustomTreeViewDragAndDrop implements ITreeDragAndDrop<ITreeItem> { } } - private debugLog(originalEvent: DragEvent) { - const types: Set<string> = new Set(); - originalEvent.dataTransfer?.types.forEach((value, index) => { - if ((originalEvent.dataTransfer?.items[index].kind === 'string') && (INTERNAL_MIME_TYPES.indexOf(value) < 0)) { - types.add(this.convertKnownMimes(value).type); - } - }); + private debugLog(types: Set<string>) { if (types.size) { this.logService.debug(`TreeView dragged mime types: ${Array.from(types).join(', ')}`); } else { @@ -1406,12 +1440,19 @@ export class CustomTreeViewDragAndDrop implements ITreeDragAndDrop<ITreeItem> { } onDragOver(data: IDragAndDropData, targetElement: ITreeItem, targetIndex: number, originalEvent: DragEvent): boolean | ITreeDragOverReaction { - this.debugLog(originalEvent); + const types: Set<string> = new Set(); + originalEvent.dataTransfer?.types.forEach((value, index) => { + if (INTERNAL_MIME_TYPES.indexOf(value) < 0) { + types.add(this.convertKnownMimes(value).type); + } + }); + + this.debugLog(types); const dndController = this.dndController; if (!dndController || !originalEvent.dataTransfer || (dndController.dropMimeTypes.length === 0)) { return false; } - const dragContainersSupportedType = originalEvent.dataTransfer.types.some((value, index) => { + const dragContainersSupportedType = Array.from(types).some((value, index) => { if (value === this.treeMimeType) { return true; } else { @@ -1442,15 +1483,15 @@ export class CustomTreeViewDragAndDrop implements ITreeDragAndDrop<ITreeItem> { return element.label ? element.label.label : (element.resourceUri ? this.labelService.getUriLabel(URI.revive(element.resourceUri)) : undefined); } - private convertKnownMimes(type: string, value?: string): { type: string; value?: string } { - let convertedValue = value; + private convertKnownMimes(type: string, kind?: string, value?: string | FileSystemHandle): { type: string; value?: string } { + let convertedValue = undefined; let convertedType = type; - switch (type) { - case DataTransfers.RESOURCES.toLowerCase(): { - convertedValue = value ? convertResourceUrlsToUriList(value) : undefined; - convertedType = Mimes.uriList; - break; - } + if (type === DataTransfers.RESOURCES.toLowerCase()) { + convertedValue = value ? convertResourceUrlsToUriList(value as string) : undefined; + convertedType = Mimes.uriList; + } else if ((type === 'Files') || (kind === 'file')) { + convertedType = Mimes.uriList; + convertedValue = value ? (value as FileSystemHandle).name : undefined; } return { type: convertedType, value: convertedValue }; } @@ -1461,8 +1502,9 @@ export class CustomTreeViewDragAndDrop implements ITreeDragAndDrop<ITreeItem> { return; } const treeDataTransfer: IDataTransfer = new Map(); - let stringCount = Array.from(originalEvent.dataTransfer.items).reduce((previous, current) => { - if (current.kind === 'string') { + const uris: URI[] = []; + let itemsCount = Array.from(originalEvent.dataTransfer.items).reduce((previous, current) => { + if ((current.kind === 'string') || (current.kind === 'file')) { return previous + 1; } return previous; @@ -1475,8 +1517,15 @@ export class CustomTreeViewDragAndDrop implements ITreeDragAndDrop<ITreeItem> { } await new Promise<void>(resolve => { function decrementStringCount() { - stringCount--; - if (stringCount === 0) { + itemsCount--; + if (itemsCount === 0) { + // Check if there are uris to add and add them + if (uris.length) { + treeDataTransfer.set(Mimes.uriList, { + asString: () => Promise.resolve(uris.map(uri => uri.toString()).join('\n')), + value: undefined + }); + } resolve(); } } @@ -1486,17 +1535,17 @@ export class CustomTreeViewDragAndDrop implements ITreeDragAndDrop<ITreeItem> { } for (const dataItem of originalEvent.dataTransfer.items) { const type = dataItem.type; - const convertedType = this.convertKnownMimes(type).type; - if (dataItem.kind === 'string') { - if ((convertedType === this.treeMimeType) || (dndController.dropMimeTypes.indexOf(convertedType) >= 0)) { + const kind = dataItem.kind; + const convertedType = this.convertKnownMimes(type, kind).type; + if ((INTERNAL_MIME_TYPES.indexOf(convertedType) < 0) + && (convertedType === this.treeMimeType) || (dndController.dropMimeTypes.indexOf(convertedType) >= 0)) { + if (dataItem.kind === 'string') { dataItem.getAsString(dataValue => { if (convertedType === this.treeMimeType) { treeSourceInfo = JSON.parse(dataValue); } - if (dataValue - && (INTERNAL_MIME_TYPES.indexOf(convertedType) < 0) - && ((convertedType === this.treeMimeType) || (dndController.dropMimeTypes.indexOf(convertedType) >= 0))) { - const converted = this.convertKnownMimes(type, dataValue); + if (dataValue) { + const converted = this.convertKnownMimes(type, kind, dataValue); treeDataTransfer.set(converted.type, { asString: () => Promise.resolve(converted.value!), value: undefined @@ -1504,9 +1553,15 @@ export class CustomTreeViewDragAndDrop implements ITreeDragAndDrop<ITreeItem> { } decrementStringCount(); }); - } else { + } else if (dataItem.kind === 'file') { + const dataValue = dataItem.getAsFile(); + if (dataValue) { + uris.push(URI.file(dataValue.path)); + } decrementStringCount(); } + } else if (dataItem.kind === 'string' || dataItem.kind === 'file') { + decrementStringCount(); } } }); diff --git a/src/vs/workbench/browser/web.api.ts b/src/vs/workbench/browser/web.api.ts index ddceb3aeeb8..effacb4a75f 100644 --- a/src/vs/workbench/browser/web.api.ts +++ b/src/vs/workbench/browser/web.api.ts @@ -14,6 +14,10 @@ import type { IWorkspaceProvider } from 'vs/workbench/services/host/browser/brow import type { IProductConfiguration } from 'vs/base/common/product'; import type { ICredentialsProvider } from 'vs/platform/credentials/common/credentials'; import type { TunnelProviderFeatures } from 'vs/platform/tunnel/common/tunnel'; +import type { IProgress, IProgressCompositeOptions, IProgressDialogOptions, IProgressNotificationOptions, IProgressOptions, IProgressStep, IProgressWindowOptions } from 'vs/platform/progress/common/progress'; +import { IObservableValue } from 'vs/base/common/observableValue'; +import { TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; /** * The `IWorkbench` interface is the API facade for web embedders @@ -41,7 +45,7 @@ export interface IWorkbench { * @returns the scheme to use for opening the associated desktop * experience via protocol handler. */ - readonly uriScheme: string; + getUriScheme(): Promise<string>; /** * Retrieve performance marks that have been collected during startup. This function @@ -61,6 +65,25 @@ export interface IWorkbench { * workbench. */ openUri(target: URI): Promise<boolean>; + + /** + * Current workbench telemetry level. + */ + readonly telemetryLevel: IObservableValue<TelemetryLevel>; + }; + + window: { + /** + * Show progress in the editor. Progress is shown while running the given callback + * and while the promise it returned isn't resolved nor rejected. + * + * @param task A callback returning a promise. + * @return A promise that resolves to the returned value of the given task result. + */ + withProgress<R>( + options: IProgressOptions | IProgressDialogOptions | IProgressNotificationOptions | IProgressWindowOptions | IProgressCompositeOptions, + task: (progress: IProgress<IProgressStep>) => Promise<R> + ): Promise<R>; }; /** @@ -538,32 +561,42 @@ export interface IDefaultView { readonly id: string; } +/** + * @deprecated use `IDefaultEditor.options` instead + */ export interface IPosition { readonly line: number; readonly column: number; } +/** + * @deprecated use `IDefaultEditor.options` instead + */ export interface IRange { - - /** - * The start position. It is before or equal to end position. - */ readonly start: IPosition; - - /** - * The end position. It is after or equal to start position. - */ readonly end: IPosition; } export interface IDefaultEditor { + readonly uri: UriComponents; - readonly selection?: IRange; + readonly options?: IEditorOptions; + readonly openOnlyIfExists?: boolean; + + /** + * @deprecated use `options` instead + */ + readonly selection?: IRange; + + /** + * @deprecated use `options.override` instead + */ readonly openWith?: string; } export interface IDefaultLayout { + readonly views?: IDefaultView[]; readonly editors?: IDefaultEditor[]; @@ -627,4 +660,3 @@ export interface IDevelopmentOptions { */ readonly enableSmokeTestDriver?: boolean; } - diff --git a/src/vs/workbench/browser/web.factory.ts b/src/vs/workbench/browser/web.factory.ts index 4b271e566d4..49c7eb489e9 100644 --- a/src/vs/workbench/browser/web.factory.ts +++ b/src/vs/workbench/browser/web.factory.ts @@ -12,6 +12,9 @@ import { mark, PerformanceMark } from 'vs/base/common/performance'; import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { DeferredPromise } from 'vs/base/common/async'; import { asArray } from 'vs/base/common/arrays'; +import { IProgress, IProgressCompositeOptions, IProgressDialogOptions, IProgressNotificationOptions, IProgressOptions, IProgressStep, IProgressWindowOptions } from 'vs/platform/progress/common/progress'; +import { IObservableValue } from 'vs/base/common/observableValue'; +import { TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; let created = false; const workbenchPromise = new DeferredPromise<IWorkbench>(); @@ -108,7 +111,7 @@ export namespace env { export async function getUriScheme(): Promise<string> { const workbench = await workbenchPromise.p; - return workbench.env.uriScheme; + return workbench.env.getUriScheme(); } /** @@ -119,4 +122,22 @@ export namespace env { return workbench.env.openUri(target); } + + export const telemetryLevel: Promise<IObservableValue<TelemetryLevel>> = + workbenchPromise.p.then(workbench => workbench.env.telemetryLevel); +} + +export namespace window { + + /** + * {@linkcode IWorkbench.window IWorkbench.window.withProgress} + */ + export async function withProgress<R>( + options: IProgressOptions | IProgressDialogOptions | IProgressNotificationOptions | IProgressWindowOptions | IProgressCompositeOptions, + task: (progress: IProgress<IProgressStep>) => Promise<R> + ): Promise<R> { + const workbench = await workbenchPromise.p; + + return workbench.window.withProgress(options, task); + } } diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index 260d8438a27..e8ca913a532 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -69,6 +69,8 @@ import { IndexedDB } from 'vs/base/browser/indexedDB'; import { BrowserCredentialsService } from 'vs/workbench/services/credentials/browser/credentialsService'; import { IWorkspace } from 'vs/workbench/services/host/browser/browserHostService'; import { WebFileSystemAccess } from 'vs/platform/files/browser/webFileSystemAccess'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IProgressService } from 'vs/platform/progress/common/progress'; export class BrowserMain extends Disposable { @@ -116,13 +118,18 @@ export class BrowserMain extends Disposable { const timerService = accessor.get(ITimerService); const openerService = accessor.get(IOpenerService); const productService = accessor.get(IProductService); + const telemetryService = accessor.get(ITelemetryService); + const progessService = accessor.get(IProgressService); return { commands: { executeCommand: (command, ...args) => commandService.executeCommand(command, ...args) }, env: { - uriScheme: productService.urlProtocol, + telemetryLevel: telemetryService.telemetryLevel, + async getUriScheme(): Promise<string> { + return productService.urlProtocol; + }, async retrievePerformanceMarks() { await timerService.whenReady(); @@ -132,6 +139,9 @@ export class BrowserMain extends Disposable { return openerService.open(uri, {}); } }, + window: { + withProgress: (options, task) => progessService.withProgress(options, task) + }, shutdown: () => lifecycleService.shutdown() }; }); @@ -243,7 +253,7 @@ export class BrowserMain extends Disposable { const workspaceTrustEnablementService = new WorkspaceTrustEnablementService(configurationService, environmentService); serviceCollection.set(IWorkspaceTrustEnablementService, workspaceTrustEnablementService); - const workspaceTrustManagementService = new WorkspaceTrustManagementService(configurationService, remoteAuthorityResolverService, storageService, uriIdentityService, environmentService, configurationService, workspaceTrustEnablementService, logService); + const workspaceTrustManagementService = new WorkspaceTrustManagementService(configurationService, remoteAuthorityResolverService, storageService, uriIdentityService, environmentService, configurationService, workspaceTrustEnablementService); serviceCollection.set(IWorkspaceTrustManagementService, workspaceTrustManagementService); // Update workspace trust so that configuration is updated accordingly diff --git a/src/vs/workbench/browser/window.ts b/src/vs/workbench/browser/window.ts index 5e94ea2bba1..7d15b8bf58f 100644 --- a/src/vs/workbench/browser/window.ts +++ b/src/vs/workbench/browser/window.ts @@ -18,6 +18,7 @@ import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { registerWindowDriver } from 'vs/platform/driver/browser/driver'; import { ILabelService } from 'vs/platform/label/common/label'; import { IOpenerService, matchesScheme } from 'vs/platform/opener/common/opener'; +import { IProductService } from 'vs/platform/product/common/productService'; import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { BrowserLifecycleService } from 'vs/workbench/services/lifecycle/browser/lifecycleService'; @@ -30,6 +31,7 @@ export class BrowserWindow extends Disposable { @ILifecycleService private readonly lifecycleService: BrowserLifecycleService, @IDialogService private readonly dialogService: IDialogService, @ILabelService private readonly labelService: ILabelService, + @IProductService private readonly productService: IProductService, @IBrowserWorkbenchEnvironmentService private readonly environmentService: IBrowserWorkbenchEnvironmentService, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService ) { @@ -65,9 +67,9 @@ export class BrowserWindow extends Disposable { this._register(addDisposableListener(this.layoutService.container, EventType.DROP, e => EventHelper.stop(e, true))); // Fullscreen (Browser) - [EventType.FULLSCREEN_CHANGE, EventType.WK_FULLSCREEN_CHANGE].forEach(event => { + for (const event of [EventType.FULLSCREEN_CHANGE, EventType.WK_FULLSCREEN_CHANGE]) { this._register(addDisposableListener(document, event, () => setFullscreen(!!detectFullscreen()))); - }); + } // Fullscreen (Native) this._register(addDisposableThrottledListener(viewport, EventType.RESIZE, () => { @@ -123,7 +125,7 @@ export class BrowserWindow extends Disposable { } private setupDriver(): void { - if (this.environmentService.options?.developmentOptions?.enableSmokeTestDriver) { + if (this.environmentService.enableSmokeTestDriver) { registerWindowDriver(); } } @@ -189,7 +191,35 @@ export class BrowserWindow extends Disposable { // but make sure to signal this as an expected unload and disable unload // handling explicitly to prevent the workbench from going down. else { - this.lifecycleService.withExpectedShutdown({ disableShutdownHandling: true }, () => window.location.href = href); + const invokeProtocolHandler = () => { + this.lifecycleService.withExpectedShutdown({ disableShutdownHandling: true }, () => window.location.href = href); + }; + + invokeProtocolHandler(); + + // We cannot know whether the protocol handler succeeded. + // Display guidance in case it did not, e.g. the app is not installed locally. + if (matchesScheme(href, this.productService.urlProtocol)) { + const showResult = await this.dialogService.show( + Severity.Info, + localize('openExternalDialogTitle', "All done. You can close this tab now."), + [ + localize('openExternalDialogButtonRetry', "Try again"), + localize('openExternalDialogButtonInstall', "Install {0}", this.productService.nameLong), + localize('openExternalDialogButtonContinue', "Continue here") + ], + { + cancelId: 2, + detail: localize('openExternalDialogDetail', "We tried opening {0} on your computer.", this.productService.nameLong) + }, + ); + + if (showResult.choice === 0) { + invokeProtocolHandler(); + } else if (showResult.choice === 1) { + await this.openerService.open(URI.parse(`http://aka.ms/vscode-install`)); + } + } } return true; diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index 5d43bf45a05..1bf0fcae7f5 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -111,6 +111,19 @@ const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Con tags: ['experimental'], description: localize('workbench.editor.preferBasedLanguageDetection', "When enabled, a language detection model that takes into account editor history will be given higher precedence."), }, + 'workbench.editor.languageDetectionHints': { + type: 'string', + default: 'always', + tags: ['experimental'], + enum: ['always', 'notebookEditors', 'textEditors', 'never'], + description: localize('workbench.editor.showLanguageDetectionHints', "When enabled, shows a status bar quick fix when the editor language doesn't match detected content language."), + enumDescriptions: [ + localize('workbench.editor.showLanguageDetectionHints.always', "Show show language detection quick fixes in both notebooks and untitled editors"), + localize('workbench.editor.showLanguageDetectionHints.notebook', "Only show language detection quick fixes in notebooks"), + localize('workbench.editor.showLanguageDetectionHints.editors', "Only show language detection quick fixes in untitled editors"), + localize('workbench.editor.showLanguageDetectionHints.never', "Never show language quick fixes"), + ] + }, 'workbench.editor.tabCloseButton': { 'type': 'string', 'enum': ['left', 'right', 'off'], @@ -464,11 +477,11 @@ const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Con '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."), 'markdownDeprecationMessage': localize({ key: 'layoutControlTypeDeprecation', comment: ['{0} is a placeholder for a setting identifier.'] }, "This setting has been deprecated in favor of {0}", '`#workbench.layoutControl.type#`') }, - 'workbench.experimental.editor.dragAndDropIntoEditor.enabled': { + 'workbench.experimental.editor.dropIntoEditor.enabled': { 'type': 'boolean', + 'default': true, 'tags': ['experimental'], - 'default': false, - 'description': localize('dragAndDropIntoEditor', "Controls whether you can drag and drop a file into an editor by holding down shift (instead of opening the file in an editor)."), + '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)."), } } }); @@ -520,6 +533,11 @@ const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Con 'default': isMacintosh ? ' \u2014 ' : ' - ', 'markdownDescription': localize("window.titleSeparator", "Separator used by `window.title`.") }, + 'window.experimental.titleMenu': { + type: 'boolean', + default: false, + description: localize('window.experimental.titleMenu', "Show window title as menu") + }, 'window.menuBarVisibility': { 'type': 'string', 'enum': ['classic', 'visible', 'toggle', 'hidden', 'compact'], @@ -586,14 +604,21 @@ const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Con 'type': 'string', 'enum': ['always', 'keyboardOnly', 'never'], 'enumDescriptions': [ - localize('window.confirmBeforeClose.always', "Always try to ask for confirmation. Note that browsers may still decide to close a tab or window without confirmation."), - localize('window.confirmBeforeClose.keyboardOnly', "Only ask for confirmation if a keybinding was detected. Note that detection may not be possible in some cases."), - localize('window.confirmBeforeClose.never', "Never explicitly ask for confirmation unless data loss is imminent.") + isWeb ? + localize('window.confirmBeforeClose.always.web', "Always try to ask for confirmation. Note that browsers may still decide to close a tab or window without confirmation.") : + localize('window.confirmBeforeClose.always', "Always ask for confirmation."), + isWeb ? + localize('window.confirmBeforeClose.keyboardOnly.web', "Only ask for confirmation if a keybinding was used to close the window. Note that detection may not be possible in some cases.") : + localize('window.confirmBeforeClose.keyboardOnly', "Only ask for confirmation if a keybinding was used."), + isWeb ? + localize('window.confirmBeforeClose.never.web', "Never explicitly ask for confirmation unless data loss is imminent.") : + localize('window.confirmBeforeClose.never', "Never explicitly ask for confirmation.") ], - 'default': isWeb && !isStandalone() ? 'keyboardOnly' : 'never', // on by default in web, unless PWA - 'description': localize('confirmBeforeCloseWeb', "Controls whether to show a confirmation dialog before closing the browser tab or window. Note that even if enabled, browsers may still decide to close a tab or window without confirmation and that this setting is only a hint that may not work in all cases."), - 'scope': ConfigurationScope.APPLICATION, - 'included': isWeb + 'default': (isWeb && !isStandalone()) ? 'keyboardOnly' : 'never', // on by default in web, unless PWA, never on desktop + 'markdownDescription': isWeb ? + localize('confirmBeforeCloseWeb', "Controls whether to show a confirmation dialog before closing the browser tab or window. Note that even if enabled, browsers may still decide to close a tab or window without confirmation and that this setting is only a hint that may not work in all cases.") : + localize('confirmBeforeClose', "Controls whether to show a confirmation dialog before closing the window or quitting the application."), + 'scope': ConfigurationScope.APPLICATION } } }); diff --git a/src/vs/workbench/browser/workbench.ts b/src/vs/workbench/browser/workbench.ts index f2c5bf51ade..27c50770822 100644 --- a/src/vs/workbench/browser/workbench.ts +++ b/src/vs/workbench/browser/workbench.ts @@ -336,7 +336,7 @@ export class Workbench extends Layout { this.restoreFontInfo(storageService, configurationService); // Create Parts - [ + for (const { id, role, classes, options } of [ { id: Parts.TITLEBAR_PART, role: 'contentinfo', classes: ['titlebar'] }, { id: Parts.BANNER_PART, role: 'banner', classes: ['banner'] }, { id: Parts.ACTIVITYBAR_PART, role: 'none', classes: ['activitybar', this.getSideBarPosition() === Position.LEFT ? 'left' : 'right'] }, // Use role 'none' for some parts to make screen readers less chatty #114892 @@ -345,11 +345,11 @@ export class Workbench extends Layout { { id: Parts.PANEL_PART, role: 'none', classes: ['panel', 'basepanel', positionToString(this.getPanelPosition())] }, { id: Parts.AUXILIARYBAR_PART, role: 'none', classes: ['auxiliarybar', 'basepanel', this.getSideBarPosition() === Position.LEFT ? 'right' : 'left'] }, { id: Parts.STATUSBAR_PART, role: 'status', classes: ['statusbar'] } - ].forEach(({ id, role, classes, options }) => { + ]) { const partContainer = this.createPart(id, role, classes); this.getPart(id).create(partContainer, options); - }); + } // Notification Handlers this.createNotificationsHandlers(instantiationService, notificationService); diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index bd610ebd12d..b12e6477782 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -9,7 +9,7 @@ import { assertIsDefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditorViewState, IDiffEditor, IDiffEditorViewState, IEditorViewState } from 'vs/editor/common/editorCommon'; -import { IEditorOptions, ITextEditorOptions, IResourceEditorInput, ITextResourceEditorInput, IBaseTextResourceEditorInput, IBaseUntypedEditorInput } from 'vs/platform/editor/common/editor'; +import { IEditorOptions, IResourceEditorInput, ITextResourceEditorInput, IBaseTextResourceEditorInput, IBaseUntypedEditorInput } from 'vs/platform/editor/common/editor'; import type { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { IInstantiationService, IConstructorSignature, ServicesAccessor, BrandedService } from 'vs/platform/instantiation/common/instantiation'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -675,7 +675,13 @@ export const enum EditorInputCapabilities { * component may decide to hide the description portion * for brevity. */ - ForceDescription = 1 << 6 + ForceDescription = 1 << 6, + + /** + * Signals that the editor supports dropping into the + * editor by holding shift. + */ + CanDropIntoEditor = 1 << 7, } export type IUntypedEditorInput = IResourceEditorInput | ITextResourceEditorInput | IUntitledTextResourceEditorInput | IResourceDiffEditorInput | IResourceSideBySideEditorInput; @@ -1334,10 +1340,9 @@ export async function pathsToEditors(paths: IPathData[] | undefined, fileService return; } - const options: ITextEditorOptions = { - selection: exists ? path.selection : undefined, - pinned: true, - override: path.editorOverrideId + const options: IEditorOptions = { + ...path.options, + pinned: true }; let input: IResourceEditorInput | IUntitledTextResourceEditorInput; diff --git a/src/vs/workbench/common/editor/diffEditorInput.ts b/src/vs/workbench/common/editor/diffEditorInput.ts index 838a0136fd8..878ebaca825 100644 --- a/src/vs/workbench/common/editor/diffEditorInput.ts +++ b/src/vs/workbench/common/editor/diffEditorInput.ts @@ -105,7 +105,10 @@ export class DiffEditorInput extends SideBySideEditorInput implements IDiffEdito // a label that resembles the difference between the two const originalMediumDescription = this.original.getDescription(Verbosity.MEDIUM); const modifiedMediumDescription = this.modified.getDescription(Verbosity.MEDIUM); - if (typeof originalMediumDescription === 'string' && typeof modifiedMediumDescription === 'string') { + if ( + (typeof originalMediumDescription === 'string' && typeof modifiedMediumDescription === 'string') && // we can only `shorten` when both sides are strings... + (originalMediumDescription || modifiedMediumDescription) // ...however never when both sides are empty strings + ) { const [shortenedOriginalMediumDescription, shortenedModifiedMediumDescription] = shorten([originalMediumDescription, modifiedMediumDescription]); mediumDescription = this.computeLabel(shortenedOriginalMediumDescription, shortenedModifiedMediumDescription); } diff --git a/src/vs/workbench/common/editor/editorGroupModel.ts b/src/vs/workbench/common/editor/editorGroupModel.ts index 3b3159e971e..8aab2bcbf6a 100644 --- a/src/vs/workbench/common/editor/editorGroupModel.ts +++ b/src/vs/workbench/common/editor/editorGroupModel.ts @@ -567,7 +567,7 @@ export class EditorGroupModel extends Disposable { kind: GroupModelChangeKind.EDITOR_MOVE, editor, oldEditorIndex: index, - editorIndex: toIndex, + editorIndex: toIndex }; this._onDidModelChange.fire(event); @@ -735,7 +735,8 @@ export class EditorGroupModel extends Disposable { this.pin(editor); // Move editor to be the last sticky editor - this.moveEditor(editor, this.sticky + 1); + const newEditorIndex = this.sticky + 1; + this.moveEditor(editor, newEditorIndex); // Adjust sticky index this.sticky++; @@ -744,7 +745,7 @@ export class EditorGroupModel extends Disposable { const event: IGroupEditorChangeEvent = { kind: GroupModelChangeKind.EDITOR_STICKY, editor, - editorIndex + editorIndex: newEditorIndex }; this._onDidModelChange.fire(event); } @@ -768,7 +769,8 @@ export class EditorGroupModel extends Disposable { } // Move editor to be the first non-sticky editor - this.moveEditor(editor, this.sticky); + const newEditorIndex = this.sticky; + this.moveEditor(editor, newEditorIndex); // Adjust sticky index this.sticky--; @@ -777,7 +779,7 @@ export class EditorGroupModel extends Disposable { const event: IGroupEditorChangeEvent = { kind: GroupModelChangeKind.EDITOR_STICKY, editor, - editorIndex + editorIndex: newEditorIndex }; this._onDidModelChange.fire(event); } diff --git a/src/vs/workbench/common/editor/resourceEditorInput.ts b/src/vs/workbench/common/editor/resourceEditorInput.ts index 062f9a50439..190a6e63db5 100644 --- a/src/vs/workbench/common/editor/resourceEditorInput.ts +++ b/src/vs/workbench/common/editor/resourceEditorInput.ts @@ -26,6 +26,10 @@ export abstract class AbstractResourceEditorInput extends EditorInput implements capabilities |= EditorInputCapabilities.Untitled; } + if (!(capabilities & EditorInputCapabilities.Readonly)) { + capabilities |= EditorInputCapabilities.CanDropIntoEditor; + } + return capabilities; } diff --git a/src/vs/workbench/common/theme.ts b/src/vs/workbench/common/theme.ts index fb7dcc2c042..bf7c818fb94 100644 --- a/src/vs/workbench/common/theme.ts +++ b/src/vs/workbench/common/theme.ts @@ -279,6 +279,27 @@ export const EDITOR_DRAG_AND_DROP_BACKGROUND = registerColor('editorGroup.dropBa hcLight: Color.fromHex('#0F4A85').transparent(0.50) }, localize('editorDragAndDropBackground', "Background color when dragging editors around. The color should have transparency so that the editor contents can still shine through.")); +export const EDITOR_DROP_INTO_PROMPT_FOREGROUND = registerColor('editorGroup.dropIntoPromptForeground', { + dark: editorWidgetForeground, + light: editorWidgetForeground, + hcDark: editorWidgetForeground, + hcLight: editorWidgetForeground +}, localize('editorDropIntoPromptForeground', "Foreground color of text shown over editors when dragging files. This text informs the user that they can hold shift to drop into the editor.")); + +export const EDITOR_DROP_INTO_PROMPT_BACKGROUND = registerColor('editorGroup.dropIntoPromptBackground', { + dark: editorWidgetBackground, + light: editorWidgetBackground, + hcDark: editorWidgetBackground, + hcLight: editorWidgetBackground +}, localize('editorDropIntoPromptBackground', "Background color of text shown over editors when dragging files. This text informs the user that they can hold shift to drop into the editor.")); + +export const EDITOR_DROP_INTO_PROMPT_BORDER = registerColor('editorGroup.dropIntoPromptBorder', { + dark: null, + light: null, + hcDark: contrastBorder, + hcLight: contrastBorder +}, localize('editorDropIntoPromptBorder', "Border color of text shown over editors when dragging files. This text informs the user that they can hold shift to drop into the editor.")); + export const SIDE_BY_SIDE_EDITOR_HORIZONTAL_BORDER = registerColor('sideBySideEditor.horizontalBorder', { dark: EDITOR_GROUP_BORDER, light: EDITOR_GROUP_BORDER, @@ -594,7 +615,7 @@ export const ACTIVITY_BAR_BADGE_BACKGROUND = registerColor('activityBarBadge.bac dark: '#007ACC', light: '#007ACC', hcDark: '#000000', - hcLight: '#007ACC' + hcLight: '#0F4A85' }, localize('activityBarBadgeBackground', "Activity notification badge background color. The activity bar is showing on the far left or right and allows to switch between views of the side bar.")); export const ACTIVITY_BAR_BADGE_FOREGROUND = registerColor('activityBarBadge.foreground', { diff --git a/src/vs/workbench/common/webview.ts b/src/vs/workbench/common/webview.ts index aa598f1133d..1db1d451aff 100644 --- a/src/vs/workbench/common/webview.ts +++ b/src/vs/workbench/common/webview.ts @@ -22,7 +22,7 @@ export const webviewResourceBaseHost = 'vscode-cdn.net'; export const webviewRootResourceAuthority = `vscode-resource.${webviewResourceBaseHost}`; -export const webviewGenericCspSource = `https://*.${webviewResourceBaseHost}`; +export const webviewGenericCspSource = `'self' https://*.${webviewResourceBaseHost}`; /** * Construct a uri that can load resources inside a webview diff --git a/src/vs/workbench/contrib/audioCues/browser/audioCues.contribution.ts b/src/vs/workbench/contrib/audioCues/browser/audioCues.contribution.ts index 4122bc5a7dd..ef9e3d8358a 100644 --- a/src/vs/workbench/contrib/audioCues/browser/audioCues.contribution.ts +++ b/src/vs/workbench/contrib/audioCues/browser/audioCues.contribution.ts @@ -41,7 +41,7 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).regis 'type': 'number', 'minimum': 0, 'maximum': 100, - 'default': 50 + 'default': 70 }, 'audioCues.lineHasBreakpoint': { 'description': localize('audioCues.lineHasBreakpoint', "Plays a sound when the active line has a breakpoint."), diff --git a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts index c796f5e0ce4..ca1856c6312 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts @@ -227,14 +227,14 @@ export class BulkEditDataSource implements IAsyncDataSource<BulkFileOperations, const range = Range.lift(edit.textEdit.textEdit.range); //prefix-math - let startTokens = textModel.getLineTokens(range.startLineNumber); + let startTokens = textModel.tokenization.getLineTokens(range.startLineNumber); let prefixLen = 23; // default value for the no tokens/grammar case for (let idx = startTokens.findTokenIndexAtOffset(range.startColumn) - 1; prefixLen < 50 && idx >= 0; idx--) { prefixLen = range.startColumn - startTokens.getStartOffset(idx); } //suffix-math - let endTokens = textModel.getLineTokens(range.endLineNumber); + let endTokens = textModel.tokenization.getLineTokens(range.endLineNumber); let suffixLen = 0; for (let idx = endTokens.findTokenIndexAtOffset(range.endColumn); suffixLen < 50 && idx < endTokens.getCount(); idx++) { suffixLen += endTokens.getEndOffset(idx) - endTokens.getStartOffset(idx); diff --git a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.css b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.css index bdd496042ce..46b11d7eedf 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.css +++ b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.css @@ -15,6 +15,10 @@ padding: 0 10px 10px; } +.simple-find-part .monaco-inputbox > .ibwrapper > input { + text-overflow: clip; +} + .monaco-workbench .simple-find-part { visibility: hidden; /* Use visibility to maintain flex layout while hidden otherwise interferes with transition */ z-index: 10; @@ -57,7 +61,11 @@ cursor: pointer; } -.monaco-workbench .simple-find-part .button.disabled { - opacity: 0.3; +.monaco-workbench div.simple-find-part div.button.disabled { + opacity: 0.3 !important; cursor: default; } + +div.simple-find-part-wrapper div.button { + border-radius: 5px; +} diff --git a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts index 1e220e2de14..ff599441598 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts @@ -12,16 +12,19 @@ import { Delayer } from 'vs/base/common/async'; import { KeyCode } from 'vs/base/common/keyCodes'; import { FindReplaceState } from 'vs/editor/contrib/find/browser/findState'; import { IMessage as InputBoxMessage } from 'vs/base/browser/ui/inputbox/inputBox'; -import { SimpleButton, findPreviousMatchIcon, findNextMatchIcon } from 'vs/editor/contrib/find/browser/findWidget'; +import { SimpleButton, findPreviousMatchIcon, findNextMatchIcon, NLS_NO_RESULTS, NLS_MATCHES_LOCATION } from 'vs/editor/contrib/find/browser/findWidget'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; -import { editorWidgetBackground, inputActiveOptionBorder, inputActiveOptionBackground, inputActiveOptionForeground, inputBackground, inputBorder, inputForeground, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, inputValidationInfoBackground, inputValidationInfoBorder, inputValidationInfoForeground, inputValidationWarningBackground, inputValidationWarningBorder, inputValidationWarningForeground, widgetShadow, editorWidgetForeground, errorForeground } from 'vs/platform/theme/common/colorRegistry'; +import { editorWidgetBackground, inputActiveOptionBorder, inputActiveOptionBackground, inputActiveOptionForeground, inputBackground, inputBorder, inputForeground, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, inputValidationInfoBackground, inputValidationInfoBorder, inputValidationInfoForeground, inputValidationWarningBackground, inputValidationWarningBorder, inputValidationWarningForeground, widgetShadow, editorWidgetForeground, errorForeground, toolbarHoverBackground, toolbarHoverOutline } from 'vs/platform/theme/common/colorRegistry'; import { IColorTheme, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { ContextScopedFindInput } from 'vs/platform/history/browser/contextScopedHistoryWidget'; import { widgetClose } from 'vs/platform/theme/common/iconRegistry'; +import * as strings from 'vs/base/common/strings'; +import { TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; const NLS_FIND_INPUT_LABEL = nls.localize('label.find', "Find"); -const NLS_FIND_INPUT_PLACEHOLDER = nls.localize('placeholder.find', "Find"); +const NLS_FIND_INPUT_PLACEHOLDER = nls.localize('placeholder.find', "Find (\u21C5 for history)"); const NLS_PREVIOUS_MATCH_BTN_LABEL = nls.localize('label.previousMatchButton', "Previous Match"); const NLS_NEXT_MATCH_BTN_LABEL = nls.localize('label.nextMatchButton', "Next Match"); const NLS_CLOSE_BTN_LABEL = nls.localize('label.closeButton', "Close"); @@ -30,6 +33,10 @@ interface IFindOptions { showOptionButtons?: boolean; checkImeCompletionState?: boolean; showResultCount?: boolean; + appendCaseSensitiveLabel?: string; + appendRegexLabel?: string; + appendWholeWordsLabel?: string; + type?: 'Terminal' | 'Webview'; } export abstract class SimpleFindWidget extends Widget { @@ -47,14 +54,15 @@ export abstract class SimpleFindWidget extends Widget { private _foundMatch: boolean = false; constructor( - @IContextViewService private readonly _contextViewService: IContextViewService, - @IContextKeyService contextKeyService: IContextKeyService, - private readonly _state: FindReplaceState = new FindReplaceState(), - private readonly _options: IFindOptions + state: FindReplaceState = new FindReplaceState(), + options: IFindOptions, + contextViewService: IContextViewService, + contextKeyService: IContextKeyService, + private readonly _keybindingService: IKeybindingService ) { super(); - this._findInput = this._register(new ContextScopedFindInput(null, this._contextViewService, { + this._findInput = this._register(new ContextScopedFindInput(null, contextViewService, { label: NLS_FIND_INPUT_LABEL, placeholder: NLS_FIND_INPUT_PLACEHOLDER, validation: (value: string): InputBoxMessage | null => { @@ -69,16 +77,19 @@ export abstract class SimpleFindWidget extends Widget { this.updateButtons(this._foundMatch); return { content: e.message }; } - } - }, contextKeyService, _options.showOptionButtons)); + }, + appendCaseSensitiveLabel: options.appendCaseSensitiveLabel && options.type === 'Terminal' ? this._getKeybinding(TerminalCommandId.ToggleFindCaseSensitive) : undefined, + appendRegexLabel: options.appendRegexLabel && options.type === 'Terminal' ? this._getKeybinding(TerminalCommandId.ToggleFindRegex) : undefined, + appendWholeWordsLabel: options.appendWholeWordsLabel && options.type === 'Terminal' ? this._getKeybinding(TerminalCommandId.ToggleFindWholeWord) : undefined + }, contextKeyService, options.showOptionButtons)); // Find History with update delayer this._updateHistoryDelayer = new Delayer<void>(500); this._register(this._findInput.onInput(async (e) => { - if (!_options.checkImeCompletionState || !this._findInput.isImeSessionInProgress) { + if (!options.checkImeCompletionState || !this._findInput.isImeSessionInProgress) { this._foundMatch = this._onInputChanged(); - if (this._options.showResultCount) { + if (options.showResultCount) { await this.updateResultCount(); } this.updateButtons(this._foundMatch); @@ -87,22 +98,22 @@ export abstract class SimpleFindWidget extends Widget { } })); - this._findInput.setRegex(!!this._state.isRegex); - this._findInput.setCaseSensitive(!!this._state.matchCase); - this._findInput.setWholeWords(!!this._state.wholeWord); + this._findInput.setRegex(!!state.isRegex); + this._findInput.setCaseSensitive(!!state.matchCase); + this._findInput.setWholeWords(!!state.wholeWord); this._register(this._findInput.onDidOptionChange(() => { - this._state.change({ + state.change({ isRegex: this._findInput.getRegex(), wholeWord: this._findInput.getWholeWords(), matchCase: this._findInput.getCaseSensitive() }, true); })); - this._register(this._state.onFindReplaceStateChange(() => { - this._findInput.setRegex(this._state.isRegex); - this._findInput.setWholeWords(this._state.wholeWord); - this._findInput.setCaseSensitive(this._state.matchCase); + this._register(state.onFindReplaceStateChange(() => { + this._findInput.setRegex(state.isRegex); + this._findInput.setWholeWords(state.wholeWord); + this._findInput.setCaseSensitive(state.matchCase); this.findFirst(); })); @@ -162,7 +173,7 @@ export abstract class SimpleFindWidget extends Widget { event.stopPropagation(); })); - if (_options?.showResultCount) { + if (options?.showResultCount) { this._domNode.classList.add('result-count'); this._register(this._findInput.onDidChange(() => { this.updateResultCount(); @@ -209,6 +220,14 @@ export abstract class SimpleFindWidget extends Widget { this._findInput.style(inputStyles); } + private _getKeybinding(actionId: string): string { + let kb = this._keybindingService?.lookupKeybinding(actionId); + if (!kb) { + return ''; + } + return ` (${kb.getLabel()})`; + } + override dispose() { super.dispose(); @@ -307,9 +326,17 @@ export abstract class SimpleFindWidget extends Widget { this._matchesCount.className = 'matchesCount'; } this._matchesCount.innerText = ''; - const label = count === undefined || count.resultCount === 0 ? `No Results` : `${count.resultIndex + 1} of ${count.resultCount}`; + let label = ''; + this._matchesCount.classList.toggle('no-results', false); + if (count?.resultCount && count?.resultCount <= 0) { + label = NLS_NO_RESULTS; + if (!!this.inputValue) { + this._matchesCount.classList.toggle('no-results', true); + } + } else if (count?.resultCount) { + label = strings.format(NLS_MATCHES_LOCATION, count.resultIndex + 1, count?.resultCount); + } this._matchesCount.appendChild(document.createTextNode(label)); - this._matchesCount.classList.toggle('no-results', !count || count.resultCount === 0); this._findInput?.domNode.insertAdjacentElement('afterend', this._matchesCount); this._foundMatch = !!count && count.resultCount > 0; } @@ -336,4 +363,23 @@ registerThemingParticipant((theme, collector) => { if (error) { collector.addRule(`.no-results.matchesCount { color: ${error}; }`); } + + const toolbarHoverBackgroundColor = theme.getColor(toolbarHoverBackground); + if (toolbarHoverBackgroundColor) { + collector.addRule(` + div.simple-find-part-wrapper div.button:hover:not(.disabled) { + background-color: ${toolbarHoverBackgroundColor}; + } + `); + } + + const toolbarHoverOutlineColor = theme.getColor(toolbarHoverOutline); + if (toolbarHoverOutlineColor) { + collector.addRule(` + div.simple-find-part-wrapper div.button:hover:not(.disabled) { + outline: 1px dashed ${toolbarHoverOutlineColor}; + outline-offset: -1px; + } + `); + } }); diff --git a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts index cec3c9c01f8..d2b4f005a8b 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts @@ -12,7 +12,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IQuickAccessRegistry, Extensions as QuickaccesExtensions } from 'vs/platform/quickinput/common/quickAccess'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor'; -import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; @@ -85,6 +85,11 @@ class GotoLineAction extends Action2 { when: null, primary: KeyMod.CtrlCmd | KeyCode.KeyG, mac: { primary: KeyMod.WinCtrl | KeyCode.KeyG } + }, + menu: { + id: MenuId.TitleMenuQuickPick, + group: '3/editorNav', + order: 2 } }); } diff --git a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts index 703b333927a..a8b90ea7be3 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts @@ -201,7 +201,7 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess item.highlights = undefined; return true; } - const score = fuzzyScore(picker.value, picker.value.toLowerCase(), 1 /*@-character*/, item.label, item.label.toLowerCase(), 0, true); + const score = fuzzyScore(picker.value, picker.value.toLowerCase(), 1 /*@-character*/, item.label, item.label.toLowerCase(), 0, { firstMatchCanBeWeak: true, boostFullMatch: true }); if (!score) { return false; } @@ -270,11 +270,15 @@ registerAction2(class GotoSymbolAction extends Action2 { weight: KeybindingWeight.WorkbenchContrib, primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyO }, - menu: { + menu: [{ id: MenuId.MenubarGoMenu, group: '4_symbol_nav', order: 1 - } + }, { + id: MenuId.TitleMenuQuickPick, + group: '3/editorNav', + order: 1 + }] }); } diff --git a/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts b/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts index 138f276bff7..9cadf75ecce 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts @@ -111,7 +111,7 @@ export class SuggestEnabledInput extends Widget implements IThemable { private readonly _onInputDidChange = new Emitter<string | undefined>(); readonly onInputDidChange: Event<string | undefined> = this._onInputDidChange.event; - protected readonly inputWidget: CodeEditorWidget; + readonly inputWidget: CodeEditorWidget; private readonly inputModel: ITextModel; protected stylingContainer: HTMLDivElement; private placeholderText: HTMLDivElement; diff --git a/src/vs/workbench/contrib/codeEditor/browser/untitledTextEditorHint.ts b/src/vs/workbench/contrib/codeEditor/browser/untitledTextEditorHint.ts index 306f9b93ce7..14c97806e09 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/untitledTextEditorHint.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/untitledTextEditorHint.ts @@ -19,6 +19,7 @@ import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { EventType as GestureEventType, Gesture } from 'vs/base/browser/touch'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; const $ = dom.$; @@ -32,6 +33,7 @@ export class UntitledTextEditorHintContribution implements IEditorContribution { constructor( private editor: ICodeEditor, + @IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService, @ICommandService private readonly commandService: ICommandService, @IConfigurationService private readonly configurationService: IConfigurationService, @IKeybindingService private readonly keybindingService: IKeybindingService, @@ -53,7 +55,7 @@ export class UntitledTextEditorHintContribution implements IEditorContribution { const model = this.editor.getModel(); if (model && model.uri.scheme === Schemas.untitled && model.getLanguageId() === PLAINTEXT_LANGUAGE_ID && configValue === 'text') { - this.untitledTextHintContentWidget = new UntitledTextEditorHintContentWidget(this.editor, this.commandService, this.configurationService, this.keybindingService); + this.untitledTextHintContentWidget = new UntitledTextEditorHintContentWidget(this.editor, this.editorGroupsService, this.commandService, this.configurationService, this.keybindingService); } } @@ -72,6 +74,7 @@ class UntitledTextEditorHintContentWidget implements IContentWidget { constructor( private readonly editor: ICodeEditor, + private readonly editorGroupsService: IEditorGroupsService, private readonly commandService: ICommandService, private readonly configurationService: IConfigurationService, private readonly keybindingService: IKeybindingService, @@ -103,6 +106,7 @@ class UntitledTextEditorHintContentWidget implements IContentWidget { if (!this.domNode) { this.domNode = $('.untitled-hint'); this.domNode.style.width = 'max-content'; + const language = $('a.language-mode'); language.style.cursor = 'pointer'; language.innerText = localize('selectAlanguage2', "Select a language"); @@ -112,10 +116,31 @@ class UntitledTextEditorHintContentWidget implements IContentWidget { language.title = localize('keyboardBindingTooltip', "{0}", languageKeybindingLabel); } this.domNode.appendChild(language); + + const or = $('span'); + or.innerText = localize('or', " or ",); + this.domNode.appendChild(or); + + const editorType = $('a.editor-type'); + editorType.style.cursor = 'pointer'; + editorType.innerText = localize('openADifferentEditor', "open a different editor"); + const selectEditorTypeKeyBinding = this.keybindingService.lookupKeybinding('welcome.showNewFileEntries'); + const selectEditorTypeKeybindingLabel = selectEditorTypeKeyBinding?.getLabel(); + if (selectEditorTypeKeybindingLabel) { + editorType.title = localize('keyboardBindingTooltip', "{0}", selectEditorTypeKeybindingLabel); + } + this.domNode.appendChild(editorType); + const toGetStarted = $('span'); - toGetStarted.innerText = localize('toGetStarted', " to get started. Start typing to dismiss, or ",); + toGetStarted.innerText = localize('toGetStarted', " to get started."); this.domNode.appendChild(toGetStarted); + this.domNode.appendChild($('br')); + + const startTyping = $('span'); + startTyping.innerText = localize('startTyping', "Start typing to dismiss or "); + this.domNode.appendChild(startTyping); + const dontShow = $('a'); dontShow.style.cursor = 'pointer'; dontShow.innerText = localize('dontshow', "don't show"); @@ -136,6 +161,21 @@ class UntitledTextEditorHintContentWidget implements IContentWidget { this.toDispose.push(dom.addDisposableListener(language, GestureEventType.Tap, languageOnClickOrTap)); this.toDispose.push(Gesture.addTarget(language)); + const chooseEditorOnClickOrTap = async (e: MouseEvent) => { + e.stopPropagation(); + + const activeEditorInput = this.editorGroupsService.activeGroup.activeEditor; + const newEditorSelected = await this.commandService.executeCommand('welcome.showNewFileEntries', { from: 'hint' }); + + // Close the active editor as long as it is untitled (swap the editors out) + if (newEditorSelected && activeEditorInput !== null && activeEditorInput.resource?.scheme === Schemas.untitled) { + this.editorGroupsService.activeGroup.closeEditor(activeEditorInput, { preserveFocus: true }); + } + }; + this.toDispose.push(dom.addDisposableListener(editorType, 'click', chooseEditorOnClickOrTap)); + this.toDispose.push(dom.addDisposableListener(editorType, GestureEventType.Tap, chooseEditorOnClickOrTap)); + this.toDispose.push(Gesture.addTarget(editorType)); + const dontShowOnClickOrTap = () => { this.configurationService.updateValue(untitledTextEditorHintSetting, 'hidden'); this.dispose(); diff --git a/src/vs/workbench/contrib/comments/browser/commentColors.ts b/src/vs/workbench/contrib/comments/browser/commentColors.ts index 3fc12217f70..127c30c86f8 100644 --- a/src/vs/workbench/contrib/comments/browser/commentColors.ts +++ b/src/vs/workbench/contrib/comments/browser/commentColors.ts @@ -5,12 +5,17 @@ import { Color } from 'vs/base/common/color'; import * as languages from 'vs/editor/common/languages'; +import { peekViewBorder } from 'vs/editor/contrib/peekView/browser/peekView'; import * as nls from 'vs/nls'; -import { contrastBorder, editorWarningForeground, editorWidgetForeground, registerColor } from 'vs/platform/theme/common/colorRegistry'; +import { contrastBorder, disabledForeground, registerColor, transparent } from 'vs/platform/theme/common/colorRegistry'; import { IColorTheme } from 'vs/platform/theme/common/themeService'; -export const resolvedCommentBorder = registerColor('comments.resolved.border', { dark: editorWidgetForeground, light: editorWidgetForeground, hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('resolvedCommentBorder', 'Color of borders and arrow for resolved comments.')); -export const unresolvedCommentBorder = registerColor('comments.unresolved.border', { dark: editorWarningForeground, light: editorWarningForeground, hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('unresolvedCommentBorder', 'Color of borders and arrow for unresolved comments.')); +const resolvedCommentBorder = registerColor('editorCommentsWidget.resolvedBorder', { dark: disabledForeground, light: disabledForeground, hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('resolvedCommentBorder', 'Color of borders and arrow for resolved comments.')); +const unresolvedCommentBorder = registerColor('editorCommentsWidget.unresolvedBorder', { dark: peekViewBorder, light: peekViewBorder, hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('unresolvedCommentBorder', 'Color of borders and arrow for unresolved comments.')); +export const commentThreadRangeBackground = registerColor('editorCommentsWidget.rangeBackground', { dark: transparent(unresolvedCommentBorder, .1), light: transparent(unresolvedCommentBorder, .1), hcDark: transparent(unresolvedCommentBorder, .1), hcLight: transparent(unresolvedCommentBorder, .1) }, nls.localize('commentThreadRangeBackground', 'Color of background for comment ranges.')); +export const commentThreadRangeBorder = registerColor('editorCommentsWidget.rangeBorder', { dark: transparent(unresolvedCommentBorder, .4), light: transparent(unresolvedCommentBorder, .4), hcDark: transparent(unresolvedCommentBorder, .4), hcLight: transparent(unresolvedCommentBorder, .4) }, nls.localize('commentThreadRangeBorder', 'Color of border for comment ranges.')); +export const commentThreadRangeActiveBackground = registerColor('editorCommentsWidget.rangeActiveBackground', { dark: transparent(unresolvedCommentBorder, .1), light: transparent(unresolvedCommentBorder, .1), hcDark: transparent(unresolvedCommentBorder, .1), hcLight: transparent(unresolvedCommentBorder, .1) }, nls.localize('commentThreadActiveRangeBackground', 'Color of background for currently selected or hovered comment range.')); +export const commentThreadRangeActiveBorder = registerColor('editorCommentsWidget.rangeActiveBorder', { dark: transparent(unresolvedCommentBorder, .4), light: transparent(unresolvedCommentBorder, .4), hcDark: transparent(unresolvedCommentBorder, .4), hcLight: transparent(unresolvedCommentBorder, .2) }, nls.localize('commentThreadActiveRangeBorder', 'Color of border for currently selected or hovered comment range.')); const commentThreadStateColors = new Map([ [languages.CommentThreadState.Unresolved, unresolvedCommentBorder], @@ -18,8 +23,10 @@ const commentThreadStateColors = new Map([ ]); export const commentThreadStateColorVar = '--comment-thread-state-color'; +export const commentViewThreadStateColorVar = '--comment-view-thread-state-color'; +export const commentThreadStateBackgroundColorVar = '--comment-thread-state-background-color'; -export function getCommentThreadStateColor(thread: languages.CommentThread, theme: IColorTheme): Color | undefined { - const colorId = thread.state !== undefined ? commentThreadStateColors.get(thread.state) : undefined; - return colorId !== undefined ? theme.getColor(colorId) : undefined; +export function getCommentThreadStateColor(state: languages.CommentThreadState | undefined, theme: IColorTheme): Color | undefined { + const colorId = (state !== undefined) ? commentThreadStateColors.get(state) : undefined; + return (colorId !== undefined) ? theme.getColor(colorId) : undefined; } diff --git a/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts b/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts index 71be0f04c36..7be06df3a41 100644 --- a/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts @@ -62,7 +62,7 @@ export class CommentGlyphWidget { return { position: { - lineNumber: range ? range.startLineNumber : this._lineNumber, + lineNumber: range ? range.endLineNumber : this._lineNumber, column: 1 }, preference: [ContentWidgetPositionPreference.EXACT] diff --git a/src/vs/workbench/contrib/comments/browser/commentNode.ts b/src/vs/workbench/contrib/comments/browser/commentNode.ts index d99f9c41a0b..db768308374 100644 --- a/src/vs/workbench/contrib/comments/browser/commentNode.ts +++ b/src/vs/workbench/contrib/comments/browser/commentNode.ts @@ -165,10 +165,11 @@ export class CommentNode<T extends IRange | ICellRange> extends Disposable { private createHeader(commentDetailsContainer: HTMLElement): void { const header = dom.append(commentDetailsContainer, dom.$(`div.comment-title.${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`)); - const author = dom.append(header, dom.$('strong.author')); + const infoContainer = dom.append(header, dom.$('comment-header-info')); + const author = dom.append(infoContainer, dom.$('strong.author')); author.innerText = this.comment.userName; - this.createTimestamp(header); - this._isPendingLabel = dom.append(header, dom.$('span.isPending')); + this.createTimestamp(infoContainer); + this._isPendingLabel = dom.append(infoContainer, dom.$('span.isPending')); if (this.comment.label) { this._isPendingLabel.innerText = this.comment.label; diff --git a/src/vs/workbench/contrib/comments/browser/commentReply.ts b/src/vs/workbench/contrib/comments/browser/commentReply.ts index 6b9c90cc2ba..cf2e81a3bb9 100644 --- a/src/vs/workbench/contrib/comments/browser/commentReply.ts +++ b/src/vs/workbench/contrib/comments/browser/commentReply.ts @@ -275,7 +275,6 @@ export class CommentReply<T extends IRange | ICellRange> extends Disposable { } private hideReplyArea() { - this.commentEditor.setValue(''); this.commentEditor.getDomNode()!.style.outline = ''; this._pendingComment = ''; this.form.classList.remove('expand'); diff --git a/src/vs/workbench/contrib/comments/browser/commentService.ts b/src/vs/workbench/contrib/comments/browser/commentService.ts index a11e4b8fb15..eed900b49eb 100644 --- a/src/vs/workbench/contrib/comments/browser/commentService.ts +++ b/src/vs/workbench/contrib/comments/browser/commentService.ts @@ -67,6 +67,7 @@ export interface ICommentService { readonly onDidUpdateCommentThreads: Event<ICommentThreadChangedEvent>; readonly onDidUpdateNotebookCommentThreads: Event<INotebookCommentThreadChangedEvent>; readonly onDidChangeActiveCommentThread: Event<CommentThread | null>; + readonly onDidChangeCurrentCommentThread: Event<CommentThread | undefined>; readonly onDidUpdateCommentingRanges: Event<{ owner: string }>; readonly onDidChangeActiveCommentingRange: Event<{ range: Range; commentingRangesInfo: CommentingRanges }>; readonly onDidSetDataProvider: Event<void>; @@ -90,6 +91,7 @@ export interface ICommentService { hasReactionHandler(owner: string): boolean; toggleReaction(owner: string, resource: URI, thread: CommentThread<IRange | ICellRange>, comment: Comment, reaction: CommentReaction): Promise<void>; setActiveCommentThread(commentThread: CommentThread<IRange | ICellRange> | null): void; + setCurrentCommentThread(commentThread: CommentThread<IRange | ICellRange> | undefined): void; } export class CommentService extends Disposable implements ICommentService { @@ -119,6 +121,9 @@ export class CommentService extends Disposable implements ICommentService { private readonly _onDidChangeActiveCommentThread = this._register(new Emitter<CommentThread | null>()); readonly onDidChangeActiveCommentThread = this._onDidChangeActiveCommentThread.event; + private readonly _onDidChangeCurrentCommentThread = this._register(new Emitter<CommentThread | undefined>()); + readonly onDidChangeCurrentCommentThread = this._onDidChangeCurrentCommentThread.event; + private readonly _onDidChangeActiveCommentingRange: Emitter<{ range: Range; commentingRangesInfo: CommentingRanges; @@ -137,6 +142,18 @@ export class CommentService extends Disposable implements ICommentService { super(); } + /** + * The current comment thread is the thread that has focus or is being hovered. + * @param commentThread + */ + setCurrentCommentThread(commentThread: CommentThread | undefined) { + this._onDidChangeCurrentCommentThread.fire(commentThread); + } + + /** + * The active comment thread is the the thread that is currently being edited. + * @param commentThread + */ setActiveCommentThread(commentThread: CommentThread | null) { this._onDidChangeActiveCommentThread.fire(commentThread); } diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadBody.ts b/src/vs/workbench/contrib/comments/browser/commentThreadBody.ts index fca40378daa..59a1d57b12b 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadBody.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadBody.ts @@ -209,8 +209,14 @@ export class CommentThreadBody<T extends IRange | ICellRange = IRange> extends D } private _updateAriaLabel() { - this._commentsElement.ariaLabel = nls.localize('commentThreadAria', "Comment thread with {0} comments. {1}.", - this._commentThread.comments?.length, this._commentThread.label); + if (this._commentThread.isDocumentCommentThread()) { + this._commentsElement.ariaLabel = nls.localize('commentThreadAria.withRange', "Comment thread with {0} comments on lines {1} through {2}. {3}.", + this._commentThread.comments?.length, this._commentThread.range.startLineNumber, this._commentThread.range.endLineNumber, + this._commentThread.label); + } else { + this._commentsElement.ariaLabel = nls.localize('commentThreadAria', "Comment thread with {0} comments. {1}.", + this._commentThread.comments?.length, this._commentThread.label); + } } private _setFocusedComment(value: number | undefined) { diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadRangeDecorator.ts b/src/vs/workbench/contrib/comments/browser/commentThreadRangeDecorator.ts new file mode 100644 index 00000000000..5af04a9fe0c --- /dev/null +++ b/src/vs/workbench/contrib/comments/browser/commentThreadRangeDecorator.ts @@ -0,0 +1,99 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { IRange } from 'vs/editor/common/core/range'; +import { IModelDecorationOptions, IModelDeltaDecoration } from 'vs/editor/common/model'; +import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; +import { ICommentInfo, ICommentService } from 'vs/workbench/contrib/comments/browser/commentService'; + +class CommentThreadRangeDecoration implements IModelDeltaDecoration { + private _decorationId: string | undefined; + + public get id(): string | undefined { + return this._decorationId; + } + + public set id(id: string | undefined) { + this._decorationId = id; + } + + constructor( + public readonly range: IRange, + public readonly options: ModelDecorationOptions) { + } +} + +export class CommentThreadRangeDecorator extends Disposable { + private static description = 'comment-thread-range-decorator'; + private decorationOptions: ModelDecorationOptions; + private activeDecorationOptions: ModelDecorationOptions; + private decorationIds: string[] = []; + private activeDecorationIds: string[] = []; + private editor: ICodeEditor | undefined; + + constructor(commentService: ICommentService) { + super(); + const decorationOptions: IModelDecorationOptions = { + description: CommentThreadRangeDecorator.description, + isWholeLine: false, + zIndex: 20, + className: 'comment-thread-range' + }; + + this.decorationOptions = ModelDecorationOptions.createDynamic(decorationOptions); + + const activeDecorationOptions: IModelDecorationOptions = { + description: CommentThreadRangeDecorator.description, + isWholeLine: false, + zIndex: 20, + className: 'comment-thread-range-current' + }; + + this.activeDecorationOptions = ModelDecorationOptions.createDynamic(activeDecorationOptions); + this._register(commentService.onDidChangeCurrentCommentThread(thread => { + if (!this.editor) { + return; + } + let newDecoration: CommentThreadRangeDecoration[] = []; + if (thread) { + const range = thread.range; + if (!((range.startLineNumber === range.endLineNumber) && (range.startColumn === range.endColumn))) { + newDecoration.push(new CommentThreadRangeDecoration(range, this.activeDecorationOptions)); + } + } + this.activeDecorationIds = this.editor.deltaDecorations(this.activeDecorationIds, newDecoration); + newDecoration.forEach((decoration, index) => decoration.id = this.decorationIds[index]); + })); + } + + public update(editor: ICodeEditor, commentInfos: ICommentInfo[]) { + const model = editor.getModel(); + if (!model) { + return; + } + this.editor = editor; + + const commentThreadRangeDecorations: CommentThreadRangeDecoration[] = []; + for (const info of commentInfos) { + info.threads.forEach(thread => { + if (thread.isDisposed) { + return; + } + const range = thread.range; + // We only want to show a range decoration when there's the range spans either multiple lines + // or, when is spans multiple characters on the sample line + if ((range.startLineNumber === range.endLineNumber) && (range.startColumn === range.endColumn)) { + return; + } + commentThreadRangeDecorations.push(new CommentThreadRangeDecoration(range, this.decorationOptions)); + }); + } + + this.decorationIds = editor.deltaDecorations(this.decorationIds, commentThreadRangeDecorations); + commentThreadRangeDecorations.forEach((decoration, index) => decoration.id = this.decorationIds[index]); + } +} diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts index b4989b77691..5759d2f3783 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts @@ -24,7 +24,7 @@ import { IColorTheme } from 'vs/platform/theme/common/themeService'; import { contrastBorder, focusBorder, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, textBlockQuoteBackground, textBlockQuoteBorder, textLinkActiveForeground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; import { PANEL_BORDER } from 'vs/workbench/common/theme'; import { IRange } from 'vs/editor/common/core/range'; -import { commentThreadStateColorVar } from 'vs/workbench/contrib/comments/browser/commentColors'; +import { commentThreadStateBackgroundColorVar, commentThreadStateColorVar } from 'vs/workbench/contrib/comments/browser/commentColors'; import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { FontInfo } from 'vs/editor/common/config/fontInfo'; @@ -109,6 +109,41 @@ export class CommentThreadWidget<T extends IRange | ICellRange = IRange> extends if (controller) { commentControllerKey.set(controller.contextValue); } + + this.currentThreadListeners(); + } + + private updateCurrentThread(hasMouse: boolean, hasFocus: boolean) { + if (hasMouse || hasFocus) { + this.commentService.setCurrentCommentThread(this.commentThread); + } else { + this.commentService.setCurrentCommentThread(undefined); + } + } + + private currentThreadListeners() { + let hasMouse = false; + let hasFocus = false; + this._register(dom.addDisposableListener(this.container, dom.EventType.MOUSE_ENTER, (e) => { + if ((<any>e).toElement === this.container) { + hasMouse = true; + this.updateCurrentThread(hasMouse, hasFocus); + } + }, true)); + this._register(dom.addDisposableListener(this.container, dom.EventType.MOUSE_LEAVE, (e) => { + if ((<any>e).fromElement === this.container) { + hasMouse = false; + this.updateCurrentThread(hasMouse, hasFocus); + } + }, true)); + this._register(dom.addDisposableListener(this.container, dom.EventType.FOCUS_IN, () => { + hasFocus = true; + this.updateCurrentThread(hasMouse, hasFocus); + }, true)); + this._register(dom.addDisposableListener(this.container, dom.EventType.FOCUS_OUT, () => { + hasFocus = false; + this.updateCurrentThread(hasMouse, hasFocus); + }, true)); } updateCommentThread(commentThread: languages.CommentThread<T>) { @@ -161,7 +196,10 @@ export class CommentThreadWidget<T extends IRange | ICellRange = IRange> extends this._onDidResize.fire(dimension); } - + override dispose() { + super.dispose(); + this.updateCurrentThread(false, false); + } private _bindCommentThreadListeners() { this._commentThreadDisposables.push(this._commentThread.onDidChangeCanReply(() => { @@ -248,6 +286,7 @@ export class CommentThreadWidget<T extends IRange | ICellRange = IRange> extends const content: string[] = []; content.push(`.monaco-editor .review-widget > .body { border-top: 1px solid var(${commentThreadStateColorVar}) }`); + content.push(`.monaco-editor .review-widget > .head { background-color: var(${commentThreadStateBackgroundColorVar}) }`); const linkColor = theme.getColor(textLinkForeground); if (linkColor) { diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts index 151a49ffbb6..c6a047179b0 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts @@ -23,10 +23,10 @@ import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { CommentThreadWidget } from 'vs/workbench/contrib/comments/browser/commentThreadWidget'; import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; -import { commentThreadStateColorVar, getCommentThreadStateColor } from 'vs/workbench/contrib/comments/browser/commentColors'; +import { commentThreadStateBackgroundColorVar, commentThreadStateColorVar, getCommentThreadStateColor } from 'vs/workbench/contrib/comments/browser/commentColors'; import { peekViewBorder } from 'vs/editor/contrib/peekView/browser/peekView'; -export function getCommentThreadWidgetStateColor(thread: languages.CommentThread, theme: IColorTheme): Color | undefined { +export function getCommentThreadWidgetStateColor(thread: languages.CommentThreadState | undefined, theme: IColorTheme): Color | undefined { return getCommentThreadStateColor(thread, theme) ?? theme.getColor(peekViewBorder); } @@ -85,7 +85,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget private readonly _globalToDispose = new DisposableStore(); private _commentThreadDisposables: IDisposable[] = []; private _contextKeyService: IContextKeyService; - private _scopedInstatiationService: IInstantiationService; + private _scopedInstantiationService: IInstantiationService; public get owner(): string { return this._owner; @@ -109,7 +109,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget super(editor, { keepEditorSelection: true }); this._contextKeyService = contextKeyService.createScoped(this.domNode); - this._scopedInstatiationService = instantiationService.createChild(new ServiceCollection( + this._scopedInstantiationService = instantiationService.createChild(new ServiceCollection( [IContextKeyService, this._contextKeyService] )); @@ -184,16 +184,16 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget protected _fillContainer(container: HTMLElement): void { this.setCssClass('review-widget'); - this._commentThreadWidget = this._scopedInstatiationService.createInstance( + this._commentThreadWidget = this._scopedInstantiationService.createInstance( CommentThreadWidget, container, this._owner, this.editor.getModel()!.uri, this._contextKeyService, - this._scopedInstatiationService, + this._scopedInstantiationService, this._commentThread as unknown as languages.CommentThread<IRange | ICellRange>, this._pendingComment, - { editor: this.editor }, + { editor: this.editor, codeBlockFontSize: '' }, this._commentOptions, { actionRunner: () => { @@ -201,7 +201,17 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget let newPosition = this.getPosition(); if (newPosition) { - this.commentService.updateCommentThreadTemplate(this.owner, this._commentThread.commentThreadHandle, new Range(newPosition.lineNumber, 1, newPosition.lineNumber, 1)); + let range: Range; + const originalRange = this._commentThread.range; + if (newPosition.lineNumber !== originalRange.endLineNumber) { + // The widget could have moved as a result of editor changes. + // We need to try to calculate the new, more correct, range for the comment. + const distance = newPosition.lineNumber - this._commentThread.range.endLineNumber; + range = new Range(originalRange.startLineNumber + distance, originalRange.startColumn, originalRange.endLineNumber + distance, originalRange.endColumn); + } else { + range = new Range(originalRange.startLineNumber, originalRange.startColumn, originalRange.endLineNumber, originalRange.endColumn); + } + this.commentService.updateCommentThreadTemplate(this.owner, this._commentThread.commentThreadHandle, range); } } }, @@ -261,7 +271,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget this._commentThreadWidget.updateCommentThread(commentThread); // Move comment glyph widget and show position if the line has changed. - const lineNumber = this._commentThread.range.startLineNumber; + const lineNumber = this._commentThread.range.endLineNumber; let shouldMoveWidget = false; if (this._commentGlyph) { if (this._commentGlyph.getPosition().position!.lineNumber !== lineNumber) { @@ -344,12 +354,13 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget this._commentThreadDisposables.push(this._commentThread.onDidChangeState(() => { const borderColor = - getCommentThreadWidgetStateColor(this._commentThread, this.themeService.getColorTheme()) || Color.transparent; + getCommentThreadWidgetStateColor(this._commentThread.state, this.themeService.getColorTheme()) || Color.transparent; this.style({ frameColor: borderColor, arrowColor: borderColor, }); this.container?.style.setProperty(commentThreadStateColorVar, `${borderColor}`); + this.container?.style.setProperty(commentThreadStateBackgroundColorVar, `${borderColor.transparent(.1)}`); })); } @@ -410,7 +421,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } private _applyTheme(theme: IColorTheme) { - const borderColor = getCommentThreadWidgetStateColor(this._commentThread, this.themeService.getColorTheme()) || Color.transparent; + const borderColor = getCommentThreadWidgetStateColor(this._commentThread.state, this.themeService.getColorTheme()) || Color.transparent; this.style({ arrowColor: borderColor, frameColor: borderColor diff --git a/src/vs/workbench/contrib/comments/browser/comments.contribution.ts b/src/vs/workbench/contrib/comments/browser/comments.contribution.ts index 0d515f2ae79..2ba619a7c0c 100644 --- a/src/vs/workbench/contrib/comments/browser/comments.contribution.ts +++ b/src/vs/workbench/contrib/comments/browser/comments.contribution.ts @@ -24,9 +24,9 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).regis markdownDeprecationMessage: nls.localize('comments.openPanel.deprecated', "This setting is deprecated in favor of `comments.openView`.") }, 'comments.openView': { - enum: ['never', 'file'], - enumDescriptions: [nls.localize('comments.openView.never', "The comments view will never be opened."), nls.localize('comments.openView.file', "The comments view will open when a file with comments is active.")], - default: 'file', + enum: ['never', 'file', 'firstFile'], + enumDescriptions: [nls.localize('comments.openView.never', "The comments view will never be opened."), nls.localize('comments.openView.file', "The comments view will open when a file with comments is active."), nls.localize('comments.openView.firstFile', "If the comments view has not been opened yet during this session it will open the first time during a session that a file with comments is active.")], + default: 'firstFile', description: nls.localize('comments.openView', "Controls when the comments view should open."), restricted: false }, diff --git a/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts b/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts index a172f418ba0..dd4fa30d7ff 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts @@ -46,6 +46,10 @@ import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { Position } from 'vs/editor/common/core/position'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { CommentThreadRangeDecorator } from 'vs/workbench/contrib/comments/browser/commentThreadRangeDecorator'; +import { commentThreadRangeActiveBackground, commentThreadRangeActiveBorder, commentThreadRangeBackground, commentThreadRangeBorder } from 'vs/workbench/contrib/comments/browser/commentColors'; +import { ICursorSelectionChangedEvent } from 'vs/editor/common/cursorEvents'; +import { CommentsPanel } from 'vs/workbench/contrib/comments/browser/commentsView'; export const ID = 'editor.contrib.review'; @@ -66,6 +70,13 @@ export class ReviewViewZone implements IViewZone { } } +interface CommentRangeAction { + ownerId: string; + extensionId: string | undefined; + label: string | undefined; + commentingRangesInfo: languages.CommentingRanges; +} + class CommentingRangeDecoration implements IModelDeltaDecoration { private _decorationId: string | undefined; private _startLineNumber: number; @@ -91,7 +102,7 @@ class CommentingRangeDecoration implements IModelDeltaDecoration { this._endLineNumber = _range.endLineNumber; } - public getCommentAction(): { ownerId: string; extensionId: string | undefined; label: string | undefined; commentingRangesInfo: languages.CommentingRanges } { + public getCommentAction(): CommentRangeAction { return { extensionId: this._extensionId, label: this._label, @@ -111,13 +122,16 @@ class CommentingRangeDecoration implements IModelDeltaDecoration { class CommentingRangeDecorator { public static description = 'commenting-range-decorator'; - private decorationOptions!: ModelDecorationOptions; - private hoverDecorationOptions!: ModelDecorationOptions; + private decorationOptions: ModelDecorationOptions; + private hoverDecorationOptions: ModelDecorationOptions; + private multilineDecorationOptions: ModelDecorationOptions; private commentingRangeDecorations: CommentingRangeDecoration[] = []; private decorationIds: string[] = []; private _editor: ICodeEditor | undefined; private _infos: ICommentInfo[] | undefined; private _lastHover: number = -1; + private _lastSelection: Range | undefined; + private _lastSelectionCursor: number | undefined; private _onDidChangeDecorationsCount: Emitter<number> = new Emitter(); public readonly onDidChangeDecorationsCount = this._onDidChangeDecorationsCount.event; @@ -137,6 +151,14 @@ class CommentingRangeDecorator { }; this.hoverDecorationOptions = ModelDecorationOptions.createDynamic(hoverDecorationOptions); + + const multilineDecorationOptions: IModelDecorationOptions = { + description: CommentingRangeDecorator.description, + isWholeLine: true, + linesDecorationsClassName: `comment-range-glyph comment-diff-added multiline-add` + }; + + this.multilineDecorationOptions = ModelDecorationOptions.createDynamic(multilineDecorationOptions); } public updateHover(hoverLine?: number) { @@ -146,27 +168,72 @@ class CommentingRangeDecorator { this._lastHover = hoverLine ?? -1; } + public updateSelection(cursorLine: number, range: Range = new Range(0, 0, 0, 0)) { + this._lastSelection = range.isEmpty() ? undefined : range; + this._lastSelectionCursor = range.isEmpty() ? undefined : cursorLine; + // Some scenarios: + // Selection is made. Emphasis should show on the drag/selection end location. + // Selection is made, then user clicks elsewhere. We should still show the decoration. + if (this._editor && this._infos) { + this._doUpdate(this._editor, this._infos, cursorLine, range); + } + } + public update(editor: ICodeEditor, commentInfos: ICommentInfo[]) { this._editor = editor; this._infos = commentInfos; this._doUpdate(editor, commentInfos); } - private _doUpdate(editor: ICodeEditor, commentInfos: ICommentInfo[], hoverLine: number = -1) { + private _doUpdate(editor: ICodeEditor, commentInfos: ICommentInfo[], emphasisLine: number = -1, selectionRange: Range | undefined = this._lastSelection) { let model = editor.getModel(); if (!model) { return; } + // If there's still a selection, use that. + emphasisLine = this._lastSelectionCursor ?? emphasisLine; + let commentingRangeDecorations: CommentingRangeDecoration[] = []; for (const info of commentInfos) { info.commentingRanges.ranges.forEach(range => { - if ((range.startLineNumber <= hoverLine) && (range.endLineNumber >= hoverLine)) { - const beforeRange = new Range(range.startLineNumber, 1, hoverLine, 1); - const hoverRange = new Range(hoverLine, 1, hoverLine, 1); - const afterRange = new Range(hoverLine, 1, range.endLineNumber, 1); + const rangeObject = new Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn); + let intersectingSelectionRange = selectionRange ? rangeObject.intersectRanges(selectionRange) : undefined; + if ((selectionRange && (emphasisLine >= 0) && intersectingSelectionRange) + // If there's only one selection line, then just drop into the else if and show an emphasis line. + && !((intersectingSelectionRange.startLineNumber === intersectingSelectionRange.endLineNumber) + && (emphasisLine === intersectingSelectionRange.startLineNumber))) { + // The emphasisLine should be the within the commenting range, even if the selection range stretches + // outside of the commenting range. + // Clip the emphasis and selection ranges to the commenting range + let intersectingEmphasisRange: Range; + if (emphasisLine <= intersectingSelectionRange.startLineNumber) { + intersectingEmphasisRange = intersectingSelectionRange.collapseToStart(); + intersectingSelectionRange = new Range(intersectingSelectionRange.startLineNumber + 1, 1, intersectingSelectionRange.endLineNumber, 1); + } else { + intersectingEmphasisRange = new Range(intersectingSelectionRange.endLineNumber, 1, intersectingSelectionRange.endLineNumber, 1); + intersectingSelectionRange = new Range(intersectingSelectionRange.startLineNumber, 1, intersectingSelectionRange.endLineNumber - 1, 1); + } + commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, intersectingSelectionRange, this.multilineDecorationOptions, info.commentingRanges, true)); + commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, intersectingEmphasisRange, this.hoverDecorationOptions, info.commentingRanges, true)); + + const beforeRangeEndLine = Math.min(intersectingEmphasisRange.startLineNumber, intersectingSelectionRange.startLineNumber) - 1; + const hasBeforeRange = rangeObject.startLineNumber <= beforeRangeEndLine; + const afterRangeStartLine = Math.max(intersectingEmphasisRange.endLineNumber, intersectingSelectionRange.endLineNumber) + 1; + const hasAfterRange = rangeObject.endLineNumber >= afterRangeStartLine; + if (hasBeforeRange) { + const beforeRange = new Range(range.startLineNumber, 1, beforeRangeEndLine, 1); + commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, beforeRange, this.decorationOptions, info.commentingRanges, true)); + } + if (hasAfterRange) { + const afterRange = new Range(afterRangeStartLine, 1, range.endLineNumber, 1); + commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, afterRange, this.decorationOptions, info.commentingRanges, true)); + } + } else if ((rangeObject.startLineNumber <= emphasisLine) && (emphasisLine <= rangeObject.endLineNumber)) { + const beforeRange = new Range(range.startLineNumber, 1, emphasisLine, 1); + const afterRange = new Range(emphasisLine, 1, range.endLineNumber, 1); commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, beforeRange, this.decorationOptions, info.commentingRanges, true)); - commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, hoverRange, this.hoverDecorationOptions, info.commentingRanges, true)); + commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, new Range(emphasisLine, 1, emphasisLine, 1), this.hoverDecorationOptions, info.commentingRanges, true)); commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, afterRange, this.decorationOptions, info.commentingRanges, true)); } else { commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, range, this.decorationOptions, info.commentingRanges)); @@ -184,28 +251,35 @@ class CommentingRangeDecorator { } } - public getMatchedCommentAction(line: number) { + public getMatchedCommentAction(commentRange: Range): CommentRangeAction[] { // keys is ownerId - const foundHoverActions = new Map<string, languages.CommentingRanges>(); - let result = []; + const foundHoverActions = new Map<string, { range: Range; action: CommentRangeAction }>(); for (const decoration of this.commentingRangeDecorations) { const range = decoration.getActiveRange(); - if (range && range.startLineNumber <= line && line <= range.endLineNumber) { - // We can have 3 commenting ranges that match from the same owner because of how - // the line hover decoration is done. We only want to use the action from 1 of them. + if (range && ((range.startLineNumber <= commentRange.startLineNumber) || (commentRange.endLineNumber <= range.endLineNumber))) { + // We can have several commenting ranges that match from the same owner because of how + // the line hover and selection decoration is done. + // The ranges must be merged so that we can see if the new commentRange fits within them. const action = decoration.getCommentAction(); - if (decoration.isHover) { - if (foundHoverActions.get(action.ownerId) === action.commentingRangesInfo) { - continue; - } else { - foundHoverActions.set(action.ownerId, action.commentingRangesInfo); - } + const alreadyFoundInfo = foundHoverActions.get(action.ownerId); + if (alreadyFoundInfo?.action.commentingRangesInfo === action.commentingRangesInfo) { + // Merge ranges. + const newRange = new Range( + range.startLineNumber < alreadyFoundInfo.range.startLineNumber ? range.startLineNumber : alreadyFoundInfo.range.startLineNumber, + range.startColumn < alreadyFoundInfo.range.startColumn ? range.startColumn : alreadyFoundInfo.range.startColumn, + range.endLineNumber > alreadyFoundInfo.range.endLineNumber ? range.endLineNumber : alreadyFoundInfo.range.endLineNumber, + range.endColumn > alreadyFoundInfo.range.endColumn ? range.endColumn : alreadyFoundInfo.range.endColumn + ); + foundHoverActions.set(action.ownerId, { range: newRange, action }); + } else { + foundHoverActions.set(action.ownerId, { range, action }); } - result.push(action); } } - return result; + return Array.from(foundHoverActions.values()).filter(action => { + return (action.range.startLineNumber <= commentRange.startLineNumber) && (commentRange.endLineNumber <= action.range.endLineNumber); + }).map(actions => actions.action); } public dispose(): void { @@ -225,11 +299,12 @@ export class CommentController implements IEditorContribution { private _commentWidgets: ReviewZoneWidget[]; private _commentInfos: ICommentInfo[]; private _commentingRangeDecorator!: CommentingRangeDecorator; + private _commentThreadRangeDecorator!: CommentThreadRangeDecorator; private mouseDownInfo: { lineNumber: number } | null = null; private _commentingRangeSpaceReserved = false; private _computePromise: CancelablePromise<Array<ICommentInfo | null>> | null; private _addInProgress!: boolean; - private _emptyThreadsToAddQueue: [number, IEditorMouseEvent | undefined][] = []; + private _emptyThreadsToAddQueue: [Range, IEditorMouseEvent | undefined][] = []; private _computeCommentingRangePromise!: CancelablePromise<ICommentInfo[]> | null; private _computeCommentingRangeScheduler!: Delayer<Array<ICommentInfo | null>> | null; private _pendingCommentCache: { [key: string]: { [key: string]: string } }; @@ -268,6 +343,8 @@ export class CommentController implements IEditorContribution { } })); + this.globalToDispose.add(this._commentThreadRangeDecorator = new CommentThreadRangeDecorator(this.commentService)); + this.globalToDispose.add(this.commentService.onDidDeleteDataProvider(ownerId => { delete this._pendingCommentCache[ownerId]; this.beginCompute(); @@ -292,6 +369,8 @@ export class CommentController implements IEditorContribution { this._editorDisposables.push(this.editor.onMouseMove(e => this.onEditorMouseMove(e))); this._editorDisposables.push(this.editor.onDidChangeCursorPosition(e => this.onEditorChangeCursorPosition(e.position))); this._editorDisposables.push(this.editor.onDidFocusEditorWidget(() => this.onEditorChangeCursorPosition(this.editor.getPosition()))); + this._editorDisposables.push(this.editor.onDidChangeCursorSelection(e => this.onEditorChangeCursorSelection(e))); + this._editorDisposables.push(this.editor.onDidBlurEditorWidget(() => this.onEditorChangeCursorSelection())); } private clearEditorListeners() { @@ -303,6 +382,13 @@ export class CommentController implements IEditorContribution { this._commentingRangeDecorator.updateHover(e.target.position?.lineNumber); } + private onEditorChangeCursorSelection(e?: ICursorSelectionChangedEvent): void { + const position = this.editor.getPosition()?.lineNumber; + if (position) { + this._commentingRangeDecorator.updateSelection(position, e?.selection); + } + } + private onEditorChangeCursorPosition(e: Position | null) { const decorations = e ? this.editor.getDecorationsInRange(Range.fromPositions(e, { column: -1, lineNumber: e.lineNumber })) : undefined; let hasCommentingRange = false; @@ -512,6 +598,13 @@ export class CommentController implements IEditorContribution { this._commentWidgets.splice(index, 1); matchedZone.dispose(); } + const infosThreads = this._commentInfos.filter(info => info.owner === e.owner)[0].threads; + for (let i = 0; i < infosThreads.length; i++) { + if (infosThreads[i] === thread) { + infosThreads.splice(i, 1); + i--; + } + } }); changed.forEach(thread => { @@ -538,21 +631,31 @@ export class CommentController implements IEditorContribution { this.displayCommentThread(e.owner, thread, pendingCommentText); this._commentInfos.filter(info => info.owner === e.owner)[0].threads.push(thread); }); - + this._commentThreadRangeDecorator.update(this.editor, commentInfo); })); - this.beginCompute().then(() => { - if (this._commentWidgets.length - && (this.configurationService.getValue<ICommentsConfiguration>(COMMENTS_SECTION).openView === 'file')) { - this.viewsService.openView(COMMENTS_VIEW_ID); + this.beginCompute(); + } + + private async openCommentsView() { + if (this._commentWidgets.length) { + if (this.configurationService.getValue<ICommentsConfiguration>(COMMENTS_SECTION).openView === 'file') { + return this.viewsService.openView(COMMENTS_VIEW_ID); + } else if (this.configurationService.getValue<ICommentsConfiguration>(COMMENTS_SECTION).openView === 'firstFile') { + const hasShownView = this.viewsService.getViewWithId<CommentsPanel>(COMMENTS_VIEW_ID)?.hasRendered; + if (!hasShownView) { + return this.viewsService.openView(COMMENTS_VIEW_ID); + } } - }); + } + return undefined; } private displayCommentThread(owner: string, thread: languages.CommentThread, pendingComment: string | null): void { const zoneWidget = this.instantiationService.createInstance(ReviewZoneWidget, this.editor, owner, thread, pendingComment); - zoneWidget.display(thread.range.startLineNumber); + zoneWidget.display(thread.range.endLineNumber); this._commentWidgets.push(zoneWidget); + this.openCommentsView(); } private onEditorMouseDown(e: IEditorMouseEvent): void { @@ -569,26 +672,33 @@ export class CommentController implements IEditorContribution { if (e.target.element.className.indexOf('comment-diff-added') >= 0) { const lineNumber = e.target.position!.lineNumber; - this.addOrToggleCommentAtLine(lineNumber, e); + // Check for selection at line number. + let range: Range = new Range(lineNumber, 1, lineNumber, 1); + const selection = this.editor.getSelection(); + if (selection && (selection.startLineNumber <= lineNumber) && (lineNumber <= selection.endLineNumber)) { + range = selection; + this.editor.setSelection(new Range(selection.endLineNumber, 1, selection.endLineNumber, 1)); + } + this.addOrToggleCommentAtLine(range, e); } } - public async addOrToggleCommentAtLine(lineNumber: number, e: IEditorMouseEvent | undefined): Promise<void> { + public async addOrToggleCommentAtLine(commentRange: Range, e: IEditorMouseEvent | undefined): Promise<void> { // If an add is already in progress, queue the next add and process it after the current one finishes to // prevent empty comment threads from being added to the same line. if (!this._addInProgress) { this._addInProgress = true; // The widget's position is undefined until the widget has been displayed, so rely on the glyph position instead - const existingCommentsAtLine = this._commentWidgets.filter(widget => widget.getGlyphPosition() === lineNumber); + const existingCommentsAtLine = this._commentWidgets.filter(widget => widget.getGlyphPosition() === commentRange.endLineNumber); if (existingCommentsAtLine.length) { - existingCommentsAtLine.forEach(widget => widget.toggleExpand(lineNumber)); + existingCommentsAtLine.forEach(widget => widget.toggleExpand(commentRange.endLineNumber)); this.processNextThreadToAdd(); return; } else { - this.addCommentAtLine(lineNumber, e); + this.addCommentAtLine(commentRange, e); } } else { - this._emptyThreadsToAddQueue.push([lineNumber, e]); + this._emptyThreadsToAddQueue.push([commentRange, e]); } } @@ -600,8 +710,8 @@ export class CommentController implements IEditorContribution { } } - public addCommentAtLine(lineNumber: number, e: IEditorMouseEvent | undefined): Promise<void> { - const newCommentInfos = this._commentingRangeDecorator.getMatchedCommentAction(lineNumber); + public addCommentAtLine(range: Range, e: IEditorMouseEvent | undefined): Promise<void> { + const newCommentInfos = this._commentingRangeDecorator.getMatchedCommentAction(range); if (!newCommentInfos.length || !this.editor.hasModel()) { return Promise.resolve(); } @@ -612,7 +722,7 @@ export class CommentController implements IEditorContribution { this.contextMenuService.showContextMenu({ getAnchor: () => anchor, - getActions: () => this.getContextMenuActions(newCommentInfos, lineNumber), + getActions: () => this.getContextMenuActions(newCommentInfos, range), getActionsContext: () => newCommentInfos.length ? newCommentInfos[0] : undefined, onHide: () => { this._addInProgress = false; } }); @@ -629,7 +739,7 @@ export class CommentController implements IEditorContribution { if (commentInfos.length) { const { ownerId } = commentInfos[0]; - this.addCommentAtLine2(lineNumber, ownerId); + this.addCommentAtLine2(range, ownerId); } }).then(() => { this._addInProgress = false; @@ -637,7 +747,7 @@ export class CommentController implements IEditorContribution { } } else { const { ownerId } = newCommentInfos[0]!; - this.addCommentAtLine2(lineNumber, ownerId); + this.addCommentAtLine2(range, ownerId); } return Promise.resolve(); @@ -656,7 +766,7 @@ export class CommentController implements IEditorContribution { return picks; } - private getContextMenuActions(commentInfos: { ownerId: string; extensionId: string | undefined; label: string | undefined; commentingRangesInfo: languages.CommentingRanges }[], lineNumber: number): IAction[] { + private getContextMenuActions(commentInfos: { ownerId: string; extensionId: string | undefined; label: string | undefined; commentingRangesInfo: languages.CommentingRanges }[], commentRange: Range): IAction[] { const actions: IAction[] = []; commentInfos.forEach(commentInfo => { @@ -668,7 +778,7 @@ export class CommentController implements IEditorContribution { undefined, true, () => { - this.addCommentAtLine2(lineNumber, ownerId); + this.addCommentAtLine2(commentRange, ownerId); return Promise.resolve(); } )); @@ -676,8 +786,7 @@ export class CommentController implements IEditorContribution { return actions; } - public addCommentAtLine2(lineNumber: number, ownerId: string) { - const range = new Range(lineNumber, 1, lineNumber, 1); + public addCommentAtLine2(range: Range, ownerId: string) { this.commentService.createCommentThreadTemplate(ownerId, this.editor.getModel()!.uri, range); this.processNextThreadToAdd(); return; @@ -742,6 +851,7 @@ export class CommentController implements IEditorContribution { }); this._commentingRangeDecorator.update(this.editor, this._commentInfos); + this._commentThreadRangeDecorator.update(this.editor, this._commentInfos); } public closeWidget(): void { @@ -848,15 +958,15 @@ CommandsRegistry.registerCommand({ return Promise.resolve(); } - const position = activeEditor.getPosition(); - return controller.addOrToggleCommentAtLine(position.lineNumber, undefined); + const position = activeEditor.getSelection(); + return controller.addOrToggleCommentAtLine(position, undefined); } }); MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: ADD_COMMENT_COMMAND, - title: nls.localize('comments.addCommand', "Add Comment on Current Line"), + title: nls.localize('comments.addCommand', "Add Comment on Current Selection"), category: 'Comments' }, when: ActiveCursorHasCommentingRange @@ -980,4 +1090,32 @@ registerThemingParticipant((theme, collector) => { if (statusBarItemActiveBackground) { collector.addRule(`.review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label:active { background-color: ${statusBarItemActiveBackground}; border: 1px solid transparent;}`); } + + const commentThreadRangeBackgroundColor = theme.getColor(commentThreadRangeBackground); + if (commentThreadRangeBackgroundColor) { + collector.addRule(`.monaco-editor .comment-thread-range { background-color: ${commentThreadRangeBackgroundColor};}`); + } + + const commentThreadRangeBorderColor = theme.getColor(commentThreadRangeBorder); + if (commentThreadRangeBorderColor) { + collector.addRule(`.monaco-editor .comment-thread-range { + border-color: ${commentThreadRangeBorderColor}; + border-width: 1px; + border-style: solid; + box-sizing: border-box; }`); + } + + const commentThreadRangeActiveBackgroundColor = theme.getColor(commentThreadRangeActiveBackground); + if (commentThreadRangeActiveBackgroundColor) { + collector.addRule(`.monaco-editor .comment-thread-range-current { background-color: ${commentThreadRangeActiveBackgroundColor};}`); + } + + const commentThreadRangeActiveBorderColor = theme.getColor(commentThreadRangeActiveBorder); + if (commentThreadRangeActiveBorderColor) { + collector.addRule(`.monaco-editor .comment-thread-range-current { + border-color: ${commentThreadRangeActiveBorderColor}; + border-width: 1px; + border-style: solid; + box-sizing: border-box; }`); + } }); diff --git a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts index e291c7d32de..3166fd47195 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts @@ -18,12 +18,15 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { WorkbenchAsyncDataTree, IListService, IWorkbenchAsyncDataTreeOptions } from 'vs/platform/list/browser/listService'; -import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { IColorTheme, IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IColorMapping } from 'vs/platform/theme/common/styler'; import { TimestampWidget } from 'vs/workbench/contrib/comments/browser/timestamp'; import { Codicon } from 'vs/base/common/codicons'; import { IMarkdownString } from 'vs/base/common/htmlContent'; +import { commentViewThreadStateColorVar, getCommentThreadStateColor } from 'vs/workbench/contrib/comments/browser/commentColors'; +import { CommentThreadState } from 'vs/editor/common/languages'; +import { Color } from 'vs/base/common/color'; export const COMMENTS_VIEW_ID = 'workbench.panel.comments'; export const COMMENTS_VIEW_TITLE = 'Comments'; @@ -50,11 +53,12 @@ interface IResourceTemplateData { interface ICommentThreadTemplateData { threadMetadata: { - icon?: HTMLElement; + icon: HTMLElement; userNames: HTMLSpanElement; timestamp: TimestampWidget; separator: HTMLElement; commentPreview: HTMLSpanElement; + range: HTMLSpanElement; }; repliesMetadata: { container: HTMLElement; @@ -121,7 +125,8 @@ export class CommentNodeRenderer implements IListRenderer<ITreeNode<CommentNode> constructor( @IOpenerService private readonly openerService: IOpenerService, - @IConfigurationService private readonly configurationService: IConfigurationService + @IConfigurationService private readonly configurationService: IConfigurationService, + @IThemeService private themeService: IThemeService ) { } renderTemplate(container: HTMLElement) { @@ -134,7 +139,8 @@ export class CommentNodeRenderer implements IListRenderer<ITreeNode<CommentNode> userNames: dom.append(metadataContainer, dom.$('.user')), timestamp: new TimestampWidget(this.configurationService, dom.append(metadataContainer, dom.$('.timestamp-container'))), separator: dom.append(metadataContainer, dom.$('.separator')), - commentPreview: dom.append(metadataContainer, dom.$('.text')) + commentPreview: dom.append(metadataContainer, dom.$('.text')), + range: dom.append(metadataContainer, dom.$('.range')) }; data.threadMetadata.separator.innerText = '\u00b7'; @@ -185,11 +191,17 @@ export class CommentNodeRenderer implements IListRenderer<ITreeNode<CommentNode> renderElement(node: ITreeNode<CommentNode>, index: number, templateData: ICommentThreadTemplateData, height: number | undefined): void { const commentCount = node.element.replies.length + 1; templateData.threadMetadata.icon?.classList.add(...ThemeIcon.asClassNameArray((commentCount === 1) ? Codicon.comment : Codicon.commentDiscussion)); + if (node.element.threadState !== undefined) { + const color = this.getCommentThreadWidgetStateColor(node.element.threadState, this.themeService.getColorTheme()); + templateData.threadMetadata.icon.style.setProperty(commentViewThreadStateColorVar, `${color}`); + templateData.threadMetadata.icon.style.color = `var(${commentViewThreadStateColorVar}`; + } templateData.threadMetadata.userNames.textContent = node.element.comment.userName; templateData.threadMetadata.timestamp.setTimestamp(node.element.comment.timestamp ? new Date(node.element.comment.timestamp) : undefined); const originalComment = node.element; templateData.threadMetadata.commentPreview.innerText = ''; + templateData.threadMetadata.commentPreview.style.height = '22px'; if (typeof originalComment.comment.body === 'string') { templateData.threadMetadata.commentPreview.innerText = originalComment.comment.body; } else { @@ -201,6 +213,12 @@ export class CommentNodeRenderer implements IListRenderer<ITreeNode<CommentNode> templateData.threadMetadata.commentPreview.title = renderedComment.element.textContent ?? ''; } + if (node.element.range.startLineNumber === node.element.range.endLineNumber) { + templateData.threadMetadata.range.textContent = nls.localize('commentLine', "[Ln {0}]", node.element.range.startLineNumber); + } else { + templateData.threadMetadata.range.textContent = nls.localize('commentRange', "[Ln {0}-{1}]", node.element.range.startLineNumber, node.element.range.endLineNumber); + } + if (!node.element.hasReply()) { templateData.repliesMetadata.container.style.display = 'none'; return; @@ -208,9 +226,13 @@ export class CommentNodeRenderer implements IListRenderer<ITreeNode<CommentNode> templateData.repliesMetadata.container.style.display = ''; templateData.repliesMetadata.count.textContent = this.getCountString(commentCount); - templateData.repliesMetadata.lastReplyDetail.textContent = nls.localize('lastReplyFrom', "Last reply from {0}", node.element.replies[node.element.replies.length - 1].comment.userName); - templateData.repliesMetadata.timestamp.setTimestamp(originalComment.comment.timestamp ? new Date(originalComment.comment.timestamp) : undefined); + const lastComment = node.element.replies[node.element.replies.length - 1].comment; + templateData.repliesMetadata.lastReplyDetail.textContent = nls.localize('lastReplyFrom', "Last reply from {0}", lastComment.userName); + templateData.repliesMetadata.timestamp.setTimestamp(lastComment.timestamp ? new Date(lastComment.timestamp) : undefined); + } + private getCommentThreadWidgetStateColor(state: CommentThreadState | undefined, theme: IColorTheme): Color | undefined { + return (state !== undefined) ? getCommentThreadStateColor(state, theme) : undefined; } disposeTemplate(templateData: ICommentThreadTemplateData): void { diff --git a/src/vs/workbench/contrib/comments/browser/commentsView.ts b/src/vs/workbench/contrib/comments/browser/commentsView.ts index ad852ea3b7e..7077d35338b 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsView.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsView.ts @@ -120,7 +120,7 @@ export class CommentsPanel extends ViewPane { const focusColor = theme.getColor(focusBorder); if (focusColor) { - content.push(`.comments-panel .commenst-panel-container a:focus { outline-color: ${focusColor}; }`); + content.push(`.comments-panel .comments-panel-container a:focus { outline-color: ${focusColor}; }`); } const codeTextForegroundColor = theme.getColor(textPreformatForeground); @@ -147,6 +147,10 @@ export class CommentsPanel extends ViewPane { } } + public get hasRendered(): boolean { + return !!this.tree; + } + public override layoutBody(height: number, width: number): void { super.layoutBody(height, width); this.tree.layout(height, width); diff --git a/src/vs/workbench/contrib/comments/browser/media/panel.css b/src/vs/workbench/contrib/comments/browser/media/panel.css index 8b2e685cb8b..37bc9a17a45 100644 --- a/src/vs/workbench/contrib/comments/browser/media/panel.css +++ b/src/vs/workbench/contrib/comments/browser/media/panel.css @@ -49,8 +49,7 @@ .comments-panel .comments-panel-container .tree-container .comment-thread-container .comment-snippet-container .count, .comments-panel .comments-panel-container .tree-container .comment-thread-container .comment-metadata-container .user { - overflow: hidden; - text-overflow: ellipsis; + min-width: fit-content; } .comments-panel .comments-panel-container .tree-container .comment-thread-container .comment-snippet-container .text { @@ -74,6 +73,11 @@ text-overflow: ellipsis; max-width: 500px; overflow: hidden; + padding-right: 5px; +} + +.comments-panel .comments-panel-container .tree-container .comment-thread-container .range { + opacity: 0.8; } .comments-panel .comments-panel-container .tree-container .comment-thread-container .comment-snippet-container .text code { diff --git a/src/vs/workbench/contrib/comments/browser/media/review.css b/src/vs/workbench/contrib/comments/browser/media/review.css index 2539c3e3960..f30e7d7109b 100644 --- a/src/vs/workbench/contrib/comments/browser/media/review.css +++ b/src/vs/workbench/contrib/comments/browser/media/review.css @@ -29,6 +29,11 @@ height: 21px; } +.review-widget .body .review-comment .comment-title .comment-header-info { + overflow: hidden; + text-overflow: ellipsis; +} + .review-widget .body .review-comment .comment-title { display: flex; width: 100%; @@ -393,6 +398,7 @@ div.preview.inline .monaco-editor .comment-range-glyph { .monaco-editor .margin-view-overlays > div:hover > .comment-range-glyph.comment-diff-added:before, .monaco-editor .margin-view-overlays .comment-range-glyph.comment-diff-added.line-hover:before, +.monaco-editor .margin-view-overlays .comment-range-glyph.comment-diff-added.multiline-add:before, .monaco-editor .comment-range-glyph.comment-thread:before { position: absolute; height: 100%; diff --git a/src/vs/workbench/contrib/comments/common/commentModel.ts b/src/vs/workbench/contrib/comments/common/commentModel.ts index d8f97430b4b..06ec27f01c3 100644 --- a/src/vs/workbench/contrib/comments/common/commentModel.ts +++ b/src/vs/workbench/contrib/comments/common/commentModel.ts @@ -5,7 +5,7 @@ import { URI } from 'vs/base/common/uri'; import { IRange } from 'vs/editor/common/core/range'; -import { Comment, CommentThread, CommentThreadChangedEvent } from 'vs/editor/common/languages'; +import { Comment, CommentThread, CommentThreadChangedEvent, CommentThreadState } from 'vs/editor/common/languages'; import { groupBy } from 'vs/base/common/arrays'; import { localize } from 'vs/nls'; @@ -21,14 +21,16 @@ export class CommentNode { replies: CommentNode[] = []; resource: URI; isRoot: boolean; + threadState?: CommentThreadState; - constructor(owner: string, threadId: string, resource: URI, comment: Comment, range: IRange) { + constructor(owner: string, threadId: string, resource: URI, comment: Comment, range: IRange, threadState: CommentThreadState | undefined) { this.owner = owner; this.threadId = threadId; this.comment = comment; this.resource = resource; this.range = range; this.isRoot = false; + this.threadState = threadState; } hasReply(): boolean { @@ -51,7 +53,7 @@ export class ResourceWithCommentThreads { public static createCommentNode(owner: string, resource: URI, commentThread: CommentThread): CommentNode { const { threadId, comments, range } = commentThread; - const commentNodes: CommentNode[] = comments!.map(comment => new CommentNode(owner, threadId!, resource, comment, range)); + const commentNodes: CommentNode[] = comments!.map(comment => new CommentNode(owner, threadId!, resource, comment, range, commentThread.state)); if (commentNodes.length > 1) { commentNodes[0].replies = commentNodes.slice(1, commentNodes.length); } diff --git a/src/vs/workbench/contrib/comments/common/commentsConfiguration.ts b/src/vs/workbench/contrib/comments/common/commentsConfiguration.ts index 1ef2ba67cb7..d3c180e69a4 100644 --- a/src/vs/workbench/contrib/comments/common/commentsConfiguration.ts +++ b/src/vs/workbench/contrib/comments/common/commentsConfiguration.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ export interface ICommentsConfiguration { - openView: 'never' | 'file'; + openView: 'never' | 'file' | 'firstFile'; useRelativeTime: boolean; } diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts index 119a41914f1..81dfae861af 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts @@ -124,6 +124,8 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { public override get capabilities(): EditorInputCapabilities { let capabilities = EditorInputCapabilities.None; + capabilities |= EditorInputCapabilities.CanDropIntoEditor; + if (!this.customEditorService.getCustomEditorCapabilities(this.viewType)?.supportsMultipleEditorsPerDocument) { capabilities |= EditorInputCapabilities.Singleton; } diff --git a/src/vs/workbench/contrib/debug/browser/baseDebugView.ts b/src/vs/workbench/contrib/debug/browser/baseDebugView.ts index acdf0b9005d..19b1e114ac0 100644 --- a/src/vs/workbench/contrib/debug/browser/baseDebugView.ts +++ b/src/vs/workbench/contrib/debug/browser/baseDebugView.ts @@ -9,10 +9,12 @@ import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { HighlightedLabel, IHighlight } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; import { IInputValidationOptions, InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; import { ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; +import { Codicon } from 'vs/base/common/codicons'; import { createMatches, FuzzyScore } from 'vs/base/common/filters'; import { once } from 'vs/base/common/functional'; import { KeyCode } from 'vs/base/common/keyCodes'; import { DisposableStore, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { localize } from 'vs/nls'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { attachInputBoxStyler } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -112,7 +114,6 @@ export function renderVariable(variable: Variable, data: IVariableTemplateData, } data.expression.classList.toggle('lazy', !!variable.presentationHint?.lazy); - data.lazyButton.title = variable.presentationHint?.lazy ? variable.value : ''; renderExpressionValue(variable, data.value, { showChanged, maxValueLength: MAX_VALUE_RENDER_LENGTH_IN_VIEWLET, @@ -156,9 +157,10 @@ export abstract class AbstractExpressionsRenderer implements ITreeRenderer<IExpr renderTemplate(container: HTMLElement): IExpressionTemplateData { const expression = dom.append(container, $('.expression')); const name = dom.append(expression, $('span.name')); - const value = dom.append(expression, $('span.value')); const lazyButton = dom.append(expression, $('span.lazy-button')); - lazyButton.textContent = `(...)`; + lazyButton.classList.add(...Codicon.eye.classNamesArray); + lazyButton.title = localize('debug.lazyButton.tooltip', "Click to expand"); + const value = dom.append(expression, $('span.value')); const label = new HighlightedLabel(name); diff --git a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts index 762b74c8201..4eab6791a7f 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts @@ -235,38 +235,48 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi const breakpoints = this.debugService.getModel().getBreakpoints({ uri, lineNumber }); if (breakpoints.length) { - // Show the dialog if there is a potential condition to be accidently lost. - // Do not show dialog on linux due to electron issue freezing the mouse #50026 - if (!env.isLinux && breakpoints.some(bp => !!bp.condition || !!bp.logMessage || !!bp.hitCondition)) { + const isShiftPressed = e.event.shiftKey; + const enabled = breakpoints.some(bp => bp.enabled); + + if (isShiftPressed) { + breakpoints.forEach(bp => this.debugService.enableOrDisableBreakpoints(!enabled, bp)); + } else if (!env.isLinux && breakpoints.some(bp => !!bp.condition || !!bp.logMessage || !!bp.hitCondition)) { + // Show the dialog if there is a potential condition to be accidently lost. + // Do not show dialog on linux due to electron issue freezing the mouse #50026 const logPoint = breakpoints.every(bp => !!bp.logMessage); const breakpointType = logPoint ? nls.localize('logPoint', "Logpoint") : nls.localize('breakpoint', "Breakpoint"); - const disable = breakpoints.some(bp => bp.enabled); - const enabling = nls.localize('breakpointHasConditionDisabled', + const disabledBreakpointDialogMessage = nls.localize( + 'breakpointHasConditionDisabled', "This {0} has a {1} that will get lost on remove. Consider enabling the {0} instead.", breakpointType.toLowerCase(), logPoint ? nls.localize('message', "message") : nls.localize('condition', "condition") ); - const disabling = nls.localize('breakpointHasConditionEnabled', + const enabledBreakpointDialogMessage = nls.localize( + 'breakpointHasConditionEnabled', "This {0} has a {1} that will get lost on remove. Consider disabling the {0} instead.", breakpointType.toLowerCase(), logPoint ? nls.localize('message', "message") : nls.localize('condition', "condition") ); - const { choice } = await this.dialogService.show(severity.Info, disable ? disabling : enabling, [ - nls.localize('removeLogPoint', "Remove {0}", breakpointType), - nls.localize('disableLogPoint', "{0} {1}", disable ? nls.localize('disable', "Disable") : nls.localize('enable', "Enable"), breakpointType), - nls.localize('cancel', "Cancel") - ], { cancelId: 2 }); + const { choice } = await this.dialogService.show( + severity.Info, + enabled ? enabledBreakpointDialogMessage : disabledBreakpointDialogMessage, + [ + nls.localize('removeLogPoint', "Remove {0}", breakpointType), + nls.localize('disableLogPoint', "{0} {1}", enabled ? nls.localize('disable', "Disable") : nls.localize('enable', "Enable"), breakpointType), + nls.localize('cancel', "Cancel") + ], + { cancelId: 2 }, + ); if (choice === 0) { breakpoints.forEach(bp => this.debugService.removeBreakpoints(bp.getId())); } if (choice === 1) { - breakpoints.forEach(bp => this.debugService.enableOrDisableBreakpoints(!disable, bp)); + breakpoints.forEach(bp => this.debugService.enableOrDisableBreakpoints(!enabled, bp)); } } else { - const enabled = breakpoints.some(bp => bp.enabled); if (!enabled) { breakpoints.forEach(bp => this.debugService.enableOrDisableBreakpoints(!enabled, bp)); } else { @@ -485,7 +495,6 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi inlineWidget }; }); - } finally { this.ignoreDecorationsChangedEvent = false; } @@ -511,6 +520,12 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi inlineWidget }; }); + + for (const d of this.breakpointDecorations) { + if (d.inlineWidget) { + this.editor.layoutContentWidget(d.inlineWidget); + } + } } private async onModelDecorationsChanged(): Promise<void> { diff --git a/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts b/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts index 4be819c4cb4..3df0cf71a74 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts @@ -48,14 +48,10 @@ export interface IPrivateBreakpointWidgetService { } const DECORATION_KEY = 'breakpointwidgetdecoration'; -function isCurlyBracketOpen(input: IActiveCodeEditor): boolean { +function isPositionInCurlyBracketBlock(input: IActiveCodeEditor): boolean { const model = input.getModel(); - const prevBracket = model.bracketPairs.findPrevBracket(input.getPosition()); - if (prevBracket && prevBracket.isOpen) { - return true; - } - - return false; + const bracketPairs = model.bracketPairs.getBracketPairsInRange(Range.fromPositions(input.getPosition())); + return bracketPairs.some(p => p.openingBracketInfo.bracketText === '{'); } function createDecorations(theme: IColorTheme, placeHolder: string): IDecorationOptions[] { @@ -251,7 +247,7 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi provideCompletionItems: (model: ITextModel, position: Position, _context: CompletionContext, token: CancellationToken): Promise<CompletionList> => { let suggestionsPromise: Promise<CompletionList>; const underlyingModel = this.editor.getModel(); - if (underlyingModel && (this.context === Context.CONDITION || (this.context === Context.LOG_MESSAGE && isCurlyBracketOpen(this.input)))) { + if (underlyingModel && (this.context === Context.CONDITION || (this.context === Context.LOG_MESSAGE && isPositionInCurlyBracketBlock(this.input)))) { suggestionsPromise = provideSuggestionItems(this.languageFeaturesService.completionProvider, underlyingModel, new Position(this.lineNumber, 1), new CompletionOptions(undefined, new Set<CompletionItemKind>().add(CompletionItemKind.Snippet)), _context, token).then(suggestions => { let overwriteBefore = 0; diff --git a/src/vs/workbench/contrib/debug/browser/callStackView.ts b/src/vs/workbench/contrib/debug/browser/callStackView.ts index 687732063d7..6a045d3ce23 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackView.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackView.ts @@ -3,50 +3,50 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { RunOnceScheduler } from 'vs/base/common/async'; import * as dom from 'vs/base/browser/dom'; -import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; -import { IDebugService, State, IStackFrame, IDebugSession, IThread, CONTEXT_CALLSTACK_ITEM_TYPE, IDebugModel, CALLSTACK_VIEW_ID, CONTEXT_DEBUG_STATE, getStateLabel, CONTEXT_STACK_FRAME_SUPPORTS_RESTART, CONTEXT_CALLSTACK_SESSION_IS_ATTACH, CONTEXT_CALLSTACK_ITEM_STOPPED, CONTEXT_CALLSTACK_SESSION_HAS_ONE_THREAD, IRawStoppedDetails } from 'vs/workbench/contrib/debug/common/debug'; -import { Thread, StackFrame, ThreadAndSessionIds } from 'vs/workbench/contrib/debug/common/debugModel'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; +import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; +import { ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree'; +import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; +import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; +import { IAsyncDataSource, ITreeContextMenuEvent, ITreeNode } from 'vs/base/browser/ui/tree/tree'; +import { Action, IAction } from 'vs/base/common/actions'; +import { RunOnceScheduler } from 'vs/base/common/async'; +import { Codicon } from 'vs/base/common/codicons'; +import { Event } from 'vs/base/common/event'; +import { createMatches, FuzzyScore, IMatch } from 'vs/base/common/filters'; +import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { posix } from 'vs/base/common/path'; +import { commonSuffixLength } from 'vs/base/common/strings'; +import { localize } from 'vs/nls'; +import { Icon } from 'vs/platform/action/common/action'; +import { createAndFillInActionBarActions, createAndFillInContextMenuActions, MenuEntryActionViewItem, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IMenuService, MenuId, MenuItemAction, MenuRegistry, registerAction2, SubmenuItemAction } from 'vs/platform/actions/common/actions'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ContextKeyExpr, ContextKeyExpression, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { MenuId, IMenu, IMenuService, MenuItemAction, SubmenuItemAction, registerAction2, MenuRegistry } from 'vs/platform/actions/common/actions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { renderViewTree } from 'vs/workbench/contrib/debug/browser/baseDebugView'; -import { IAction, Action } from 'vs/base/common/actions'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IContextKey, IContextKeyService, ContextKeyExpr, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; -import { ViewPane, ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { ILabelService } from 'vs/platform/label/common/label'; -import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; -import { createAndFillInContextMenuActions, createAndFillInActionBarActions, MenuEntryActionViewItem, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; -import { ITreeNode, ITreeContextMenuEvent, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree'; import { WorkbenchCompressibleAsyncDataTree } from 'vs/platform/list/browser/listService'; -import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; -import { createMatches, FuzzyScore, IMatch } from 'vs/base/common/filters'; -import { Event } from 'vs/base/common/event'; -import { dispose, IDisposable } from 'vs/base/common/lifecycle'; -import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; -import { isSessionAttach } from 'vs/workbench/contrib/debug/common/debugUtils'; -import { STOP_ID, STOP_LABEL, DISCONNECT_ID, DISCONNECT_LABEL, RESTART_SESSION_ID, RESTART_LABEL, STEP_OVER_ID, STEP_OVER_LABEL, STEP_INTO_LABEL, STEP_INTO_ID, STEP_OUT_LABEL, STEP_OUT_ID, PAUSE_ID, PAUSE_LABEL, CONTINUE_ID, CONTINUE_LABEL } from 'vs/workbench/contrib/debug/browser/debugCommands'; -import { IViewDescriptorService } from 'vs/workbench/common/views'; -import { textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; -import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { INotificationService } from 'vs/platform/notification/common/notification'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; import { attachStylerCallback } from 'vs/platform/theme/common/styler'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { commonSuffixLength } from 'vs/base/common/strings'; -import { posix } from 'vs/base/common/path'; -import { ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree'; -import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; -import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; +import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { ViewAction, ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; +import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; +import { IViewDescriptorService } from 'vs/workbench/common/views'; +import { renderViewTree } from 'vs/workbench/contrib/debug/browser/baseDebugView'; +import { CONTINUE_ID, CONTINUE_LABEL, DISCONNECT_ID, DISCONNECT_LABEL, PAUSE_ID, PAUSE_LABEL, RESTART_LABEL, RESTART_SESSION_ID, STEP_INTO_ID, STEP_INTO_LABEL, STEP_OUT_ID, STEP_OUT_LABEL, STEP_OVER_ID, STEP_OVER_LABEL, STOP_ID, STOP_LABEL } from 'vs/workbench/contrib/debug/browser/debugCommands'; import * as icons from 'vs/workbench/contrib/debug/browser/debugIcons'; -import { localize } from 'vs/nls'; -import { Codicon } from 'vs/base/common/codicons'; -import { Icon } from 'vs/platform/action/common/action'; +import { createDisconnectMenuItemAction } from 'vs/workbench/contrib/debug/browser/debugToolBar'; +import { CALLSTACK_VIEW_ID, CONTEXT_CALLSTACK_ITEM_STOPPED, CONTEXT_CALLSTACK_ITEM_TYPE, CONTEXT_CALLSTACK_SESSION_HAS_ONE_THREAD, CONTEXT_CALLSTACK_SESSION_IS_ATTACH, CONTEXT_DEBUG_STATE, CONTEXT_STACK_FRAME_SUPPORTS_RESTART, getStateLabel, IDebugModel, IDebugService, IDebugSession, IRawStoppedDetails, IStackFrame, IThread, State } from 'vs/workbench/contrib/debug/common/debug'; +import { StackFrame, Thread, ThreadAndSessionIds } from 'vs/workbench/contrib/debug/common/debugModel'; +import { isSessionAttach } from 'vs/workbench/contrib/debug/common/debugUtils'; const $ = dom.$; @@ -136,14 +136,9 @@ export class CallStackView extends ViewPane { private needsRefresh = false; private ignoreSelectionChangedEvent = false; private ignoreFocusStackFrameEvent = false; - private callStackItemType: IContextKey<string>; - private callStackSessionIsAttach: IContextKey<boolean>; - private callStackItemStopped: IContextKey<boolean>; - private stackFrameSupportsRestart: IContextKey<boolean>; - private sessionHasOneThread: IContextKey<boolean>; + private dataSource!: CallStackDataSource; private tree!: WorkbenchCompressibleAsyncDataTree<IDebugModel, CallStackItem, FuzzyScore>; - private menu: IMenu; private autoExpandedSessions = new Set<IDebugSession>(); private selectionNeedsUpdate = false; @@ -154,23 +149,14 @@ export class CallStackView extends ViewPane { @IKeybindingService keybindingService: IKeybindingService, @IInstantiationService instantiationService: IInstantiationService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, - @IEditorService private readonly editorService: IEditorService, @IConfigurationService configurationService: IConfigurationService, - @IMenuService menuService: IMenuService, @IContextKeyService contextKeyService: IContextKeyService, @IOpenerService openerService: IOpenerService, @IThemeService themeService: IThemeService, - @ITelemetryService telemetryService: ITelemetryService + @ITelemetryService telemetryService: ITelemetryService, + @IMenuService private readonly menuService: IMenuService, ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); - this.callStackItemType = CONTEXT_CALLSTACK_ITEM_TYPE.bindTo(contextKeyService); - this.callStackSessionIsAttach = CONTEXT_CALLSTACK_SESSION_IS_ATTACH.bindTo(contextKeyService); - this.stackFrameSupportsRestart = CONTEXT_STACK_FRAME_SUPPORTS_RESTART.bindTo(contextKeyService); - this.callStackItemStopped = CONTEXT_CALLSTACK_ITEM_STOPPED.bindTo(contextKeyService); - this.sessionHasOneThread = CONTEXT_CALLSTACK_SESSION_HAS_ONE_THREAD.bindTo(contextKeyService); - - this.menu = menuService.createMenu(MenuId.DebugCallStackContext, contextKeyService); - this._register(this.menu); // Create scheduler to prevent unnecessary flashing of tree when reacting to changes this.onCallStackChangeScheduler = this._register(new RunOnceScheduler(async () => { @@ -239,9 +225,9 @@ export class CallStackView extends ViewPane { this.dataSource = new CallStackDataSource(this.debugService); this.tree = <WorkbenchCompressibleAsyncDataTree<IDebugModel, CallStackItem, FuzzyScore>>this.instantiationService.createInstance(WorkbenchCompressibleAsyncDataTree, 'CallStackView', treeContainer, new CallStackDelegate(), new CallStackCompressionDelegate(this.debugService), [ - new SessionsRenderer(this.menu, this.callStackItemType, this.callStackSessionIsAttach, this.callStackItemStopped, this.sessionHasOneThread, this.instantiationService), - new ThreadsRenderer(this.menu, this.callStackItemType, this.callStackItemStopped), - this.instantiationService.createInstance(StackFramesRenderer, this.callStackItemType), + this.instantiationService.createInstance(SessionsRenderer), + this.instantiationService.createInstance(ThreadsRenderer), + this.instantiationService.createInstance(StackFramesRenderer), new ErrorsRenderer(), new LoadAllRenderer(this.themeService), new ShowMoreRenderer(this.themeService) @@ -299,10 +285,10 @@ export class CallStackView extends ViewPane { return; } - const focusStackFrame = (stackFrame: IStackFrame | undefined, thread: IThread | undefined, session: IDebugSession) => { + const focusStackFrame = (stackFrame: IStackFrame | undefined, thread: IThread | undefined, session: IDebugSession, options: { explicit?: boolean; preserveFocus?: boolean; sideBySide?: boolean; pinned?: boolean } = {}) => { this.ignoreFocusStackFrameEvent = true; try { - this.debugService.focusStackFrame(stackFrame, thread, session, true); + this.debugService.focusStackFrame(stackFrame, thread, session, { ...options, ...{ explicit: true } }); } finally { this.ignoreFocusStackFrameEvent = false; } @@ -310,8 +296,12 @@ export class CallStackView extends ViewPane { const element = e.element; if (element instanceof StackFrame) { - focusStackFrame(element, element.thread, element.thread.session); - element.openInEditor(this.editorService, e.editorOptions.preserveFocus, e.sideBySide, e.editorOptions.pinned); + const opts = { + preserveFocus: e.editorOptions.preserveFocus, + sideBySide: e.sideBySide, + pinned: e.editorOptions.pinned + }; + focusStackFrame(element, element.thread, element.thread.session, opts); } if (element instanceof Thread) { focusStackFrame(undefined, element, element.session); @@ -455,22 +445,21 @@ export class CallStackView extends ViewPane { private onContextMenu(e: ITreeContextMenuEvent<CallStackItem>): void { const element = e.element; - this.stackFrameSupportsRestart.reset(); + let overlay: [string, any][] = []; if (isDebugSession(element)) { - this.callStackItemType.set('session'); + overlay = getSessionContextOverlay(element); } else if (element instanceof Thread) { - this.callStackItemType.set('thread'); + overlay = getThreadContextOverlay(element); } else if (element instanceof StackFrame) { - this.callStackItemType.set('stackFrame'); - this.stackFrameSupportsRestart.set(element.canRestart); - } else { - this.callStackItemType.reset(); + overlay = getStackFrameContextOverlay(element); } const primary: IAction[] = []; const secondary: IAction[] = []; const result = { primary, secondary }; - const actionsDisposable = createAndFillInContextMenuActions(this.menu, { arg: getContextForContributedActions(element), shouldForwardArgs: true }, result, 'inline'); + const contextKeyService = this.contextKeyService.createOverlay(overlay); + const menu = this.menuService.createMenu(MenuId.DebugCallStackContext, contextKeyService); + const actionsDisposable = createAndFillInContextMenuActions(menu, { arg: getContextForContributedActions(element), shouldForwardArgs: true }, result, 'inline'); this.contextMenuService.showContextMenu({ getAnchor: () => e.anchor, @@ -487,7 +476,8 @@ interface IThreadTemplateData { stateLabel: HTMLSpanElement; label: HighlightedLabel; actionBar: ActionBar; - elementDisposable: IDisposable[]; + elementDisposable: DisposableStore; + templateDisposable: IDisposable; } interface ISessionTemplateData { @@ -496,7 +486,8 @@ interface ISessionTemplateData { stateLabel: HTMLSpanElement; label: HighlightedLabel; actionBar: ActionBar; - elementDisposable: IDisposable[]; + elementDisposable: DisposableStore; + templateDisposable: IDisposable; } interface IErrorTemplateData { @@ -515,18 +506,25 @@ interface IStackFrameTemplateData { lineNumber: HTMLElement; label: HighlightedLabel; actionBar: ActionBar; + templateDisposable: IDisposable; +} + +function getSessionContextOverlay(session: IDebugSession): [string, any][] { + return [ + [CONTEXT_CALLSTACK_ITEM_TYPE.key, 'session'], + [CONTEXT_CALLSTACK_SESSION_IS_ATTACH.key, isSessionAttach(session)], + [CONTEXT_CALLSTACK_ITEM_STOPPED.key, session.state === State.Stopped], + [CONTEXT_CALLSTACK_SESSION_HAS_ONE_THREAD.key, session.getAllThreads().length === 1], + ]; } class SessionsRenderer implements ICompressibleTreeRenderer<IDebugSession, FuzzyScore, ISessionTemplateData> { static readonly ID = 'session'; constructor( - private menu: IMenu, - private callStackItemType: IContextKey<string>, - private callStackSessionIsAttach: IContextKey<boolean>, - private callStackItemStopped: IContextKey<boolean>, - private sessionHasOneThread: IContextKey<boolean>, - private readonly instantiationService: IInstantiationService + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IMenuService private readonly menuService: IMenuService, ) { } get templateId(): string { @@ -539,8 +537,19 @@ class SessionsRenderer implements ICompressibleTreeRenderer<IDebugSession, Fuzzy const name = dom.append(session, $('.name')); const stateLabel = dom.append(session, $('span.state.label.monaco-count-badge.long')); const label = new HighlightedLabel(name); - const actionBar = new ActionBar(session, { + const templateDisposable = new DisposableStore(); + + const stopActionViewItemDisposables = templateDisposable.add(new DisposableStore()); + const actionBar = templateDisposable.add(new ActionBar(session, { actionViewItemProvider: action => { + if ((action.id === STOP_ID || action.id === DISCONNECT_ID) && action instanceof MenuItemAction) { + stopActionViewItemDisposables.clear(); + const item = this.instantiationService.invokeFunction(accessor => createDisconnectMenuItemAction(action as MenuItemAction, stopActionViewItemDisposables, accessor)); + if (item) { + return item; + } + } + if (action instanceof MenuItemAction) { return this.instantiationService.createInstance(MenuEntryActionViewItem, action, undefined); } else if (action instanceof SubmenuItemAction) { @@ -549,9 +558,10 @@ class SessionsRenderer implements ICompressibleTreeRenderer<IDebugSession, Fuzzy return undefined; } - }); + })); - return { session, name, stateLabel, label, actionBar, elementDisposable: [] }; + const elementDisposable = templateDisposable.add(new DisposableStore()); + return { session, name, stateLabel, label, actionBar, elementDisposable, templateDisposable }; } renderElement(element: ITreeNode<IDebugSession, FuzzyScore>, _: number, data: ISessionTemplateData): void { @@ -569,20 +579,28 @@ class SessionsRenderer implements ICompressibleTreeRenderer<IDebugSession, Fuzzy data.label.set(session.getLabel(), matches); const stoppedDetails = session.getStoppedDetails(); const thread = session.getAllThreads().find(t => t.stopped); - const primary: IAction[] = []; - const secondary: IAction[] = []; - const result = { primary, secondary }; - this.callStackItemType.set('session'); - this.callStackItemStopped.set(session.state === State.Stopped); - this.sessionHasOneThread.set(session.getAllThreads().length === 1); - this.callStackSessionIsAttach.set(isSessionAttach(session)); - data.elementDisposable.push(createAndFillInActionBarActions(this.menu, { arg: getContextForContributedActions(session), shouldForwardArgs: true }, result, 'inline')); - data.actionBar.clear(); - data.actionBar.push(primary, { icon: true, label: false }); - // We need to set our internal context on the action bar, since our commands depend on that one - // While the external context our extensions rely on - data.actionBar.context = getContext(session); + const contextKeyService = this.contextKeyService.createOverlay(getSessionContextOverlay(session)); + const menu = data.elementDisposable.add(this.menuService.createMenu(MenuId.DebugCallStackContext, contextKeyService)); + + const menuDisposables = data.elementDisposable.add(new DisposableStore()); + const setupActionBar = () => { + menuDisposables.clear(); + data.actionBar.clear(); + + const primary: IAction[] = []; + const secondary: IAction[] = []; + const result = { primary, secondary }; + + menuDisposables.add(createAndFillInActionBarActions(menu, { arg: getContextForContributedActions(session), shouldForwardArgs: true }, result, 'inline')); + data.actionBar.push(primary, { icon: true, label: false }); + // We need to set our internal context on the action bar, since our commands depend on that one + // While the external context our extensions rely on + data.actionBar.context = getContext(session); + }; + data.elementDisposable.add(menu.onDidChange(() => setupActionBar())); + setupActionBar(); + data.stateLabel.style.display = ''; if (stoppedDetails) { @@ -600,21 +618,27 @@ class SessionsRenderer implements ICompressibleTreeRenderer<IDebugSession, Fuzzy } disposeTemplate(templateData: ISessionTemplateData): void { - templateData.actionBar.dispose(); + templateData.templateDisposable.dispose(); } disposeElement(_element: ITreeNode<IDebugSession, FuzzyScore>, _: number, templateData: ISessionTemplateData): void { - dispose(templateData.elementDisposable); + templateData.elementDisposable.clear(); } } +function getThreadContextOverlay(thread: IThread): [string, any][] { + return [ + [CONTEXT_CALLSTACK_ITEM_TYPE.key, 'thread'], + [CONTEXT_CALLSTACK_ITEM_STOPPED.key, thread.stopped] + ]; +} + class ThreadsRenderer implements ICompressibleTreeRenderer<IThread, FuzzyScore, IThreadTemplateData> { static readonly ID = 'thread'; constructor( - private menu: IMenu, - private callStackItemType: IContextKey<string>, - private callStackItemStopped: IContextKey<boolean> + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IMenuService private readonly menuService: IMenuService, ) { } get templateId(): string { @@ -626,10 +650,13 @@ class ThreadsRenderer implements ICompressibleTreeRenderer<IThread, FuzzyScore, const name = dom.append(thread, $('.name')); const stateLabel = dom.append(thread, $('span.state.label.monaco-count-badge.long')); const label = new HighlightedLabel(name); - const actionBar = new ActionBar(thread); - const elementDisposable: IDisposable[] = []; - return { thread, name, stateLabel, label, actionBar, elementDisposable }; + const templateDisposable = new DisposableStore(); + + const actionBar = templateDisposable.add(new ActionBar(thread)); + const elementDisposable = templateDisposable.add(new DisposableStore()); + + return { thread, name, stateLabel, label, actionBar, elementDisposable, templateDisposable }; } renderElement(element: ITreeNode<IThread, FuzzyScore>, _index: number, data: IThreadTemplateData): void { @@ -639,13 +666,26 @@ class ThreadsRenderer implements ICompressibleTreeRenderer<IThread, FuzzyScore, data.stateLabel.textContent = thread.stateLabel; data.stateLabel.classList.toggle('exception', thread.stoppedDetails?.reason === 'exception'); - data.actionBar.clear(); - this.callStackItemType.set('thread'); - this.callStackItemStopped.set(thread.stopped); - const primary: IAction[] = []; - const result = { primary, secondary: [] }; - data.elementDisposable.push(createAndFillInActionBarActions(this.menu, { arg: getContextForContributedActions(thread), shouldForwardArgs: true }, result, 'inline')); - data.actionBar.push(primary, { icon: true, label: false }); + const contextKeyService = this.contextKeyService.createOverlay(getThreadContextOverlay(thread)); + const menu = data.elementDisposable.add(this.menuService.createMenu(MenuId.DebugCallStackContext, contextKeyService)); + + const menuDisposables = data.elementDisposable.add(new DisposableStore()); + const setupActionBar = () => { + menuDisposables.clear(); + data.actionBar.clear(); + + const primary: IAction[] = []; + const secondary: IAction[] = []; + const result = { primary, secondary }; + + menuDisposables.add(createAndFillInActionBarActions(menu, { arg: getContextForContributedActions(thread), shouldForwardArgs: true }, result, 'inline')); + data.actionBar.push(primary, { icon: true, label: false }); + // We need to set our internal context on the action bar, since our commands depend on that one + // While the external context our extensions rely on + data.actionBar.context = getContext(thread); + }; + data.elementDisposable.add(menu.onDidChange(() => setupActionBar())); + setupActionBar(); } renderCompressedElements(_node: ITreeNode<ICompressedTreeNode<IThread>, FuzzyScore>, _index: number, _templateData: IThreadTemplateData, _height: number | undefined): void { @@ -653,19 +693,25 @@ class ThreadsRenderer implements ICompressibleTreeRenderer<IThread, FuzzyScore, } disposeElement(_element: any, _index: number, templateData: IThreadTemplateData): void { - dispose(templateData.elementDisposable); + templateData.elementDisposable.clear(); } disposeTemplate(templateData: IThreadTemplateData): void { - templateData.actionBar.dispose(); + templateData.templateDisposable.dispose(); } } +function getStackFrameContextOverlay(stackFrame: IStackFrame): [string, any][] { + return [ + [CONTEXT_CALLSTACK_ITEM_TYPE.key, 'stackFrame'], + [CONTEXT_STACK_FRAME_SUPPORTS_RESTART.key, stackFrame.canRestart] + ]; +} + class StackFramesRenderer implements ICompressibleTreeRenderer<IStackFrame, FuzzyScore, IStackFrameTemplateData> { static readonly ID = 'stackFrame'; constructor( - private callStackItemType: IContextKey<string>, @ILabelService private readonly labelService: ILabelService, @INotificationService private readonly notificationService: INotificationService, ) { } @@ -682,9 +728,11 @@ class StackFramesRenderer implements ICompressibleTreeRenderer<IStackFrame, Fuzz const wrapper = dom.append(file, $('span.line-number-wrapper')); const lineNumber = dom.append(wrapper, $('span.line-number.monaco-count-badge')); const label = new HighlightedLabel(labelDiv); - const actionBar = new ActionBar(stackFrame); - return { file, fileName, label, lineNumber, stackFrame, actionBar }; + const templateDisposable = new DisposableStore(); + const actionBar = templateDisposable.add(new ActionBar(stackFrame)); + + return { file, fileName, label, lineNumber, stackFrame, actionBar, templateDisposable }; } renderElement(element: ITreeNode<IStackFrame, FuzzyScore>, index: number, data: IStackFrameTemplateData): void { @@ -712,7 +760,6 @@ class StackFramesRenderer implements ICompressibleTreeRenderer<IStackFrame, Fuzz } data.actionBar.clear(); - this.callStackItemType.set('stackFrame'); if (hasActions) { const action = new Action('debug.callStack.restartFrame', localize('restartFrame', "Restart Frame"), ThemeIcon.asClassName(icons.debugRestartFrame), true, async () => { try { diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index 11f4ef0ce63..d595b7768d1 100644 --- a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts @@ -16,11 +16,11 @@ import { CallStackView } from 'vs/workbench/contrib/debug/browser/callStackView' import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { IDebugService, VIEWLET_ID, DEBUG_PANEL_ID, CONTEXT_IN_DEBUG_MODE, INTERNAL_CONSOLE_OPTIONS_SCHEMA, - CONTEXT_DEBUG_STATE, VARIABLES_VIEW_ID, CALLSTACK_VIEW_ID, WATCH_VIEW_ID, BREAKPOINTS_VIEW_ID, LOADED_SCRIPTS_VIEW_ID, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_CALLSTACK_ITEM_TYPE, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_DEBUG_UX, BREAKPOINT_EDITOR_CONTRIBUTION_ID, REPL_VIEW_ID, CONTEXT_BREAKPOINTS_EXIST, EDITOR_CONTRIBUTION_ID, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_SET_VARIABLE_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT, getStateLabel, State, CONTEXT_WATCH_ITEM_TYPE, CONTEXT_STACK_FRAME_SUPPORTS_RESTART, CONTEXT_BREAK_WHEN_VALUE_IS_READ_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_ACCESSED_SUPPORTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED, DISASSEMBLY_VIEW_ID, CONTEXT_SET_EXPRESSION_SUPPORTED, CONTEXT_VARIABLE_IS_READONLY, CONTEXT_CAN_VIEW_MEMORY, + CONTEXT_DEBUG_STATE, VARIABLES_VIEW_ID, CALLSTACK_VIEW_ID, WATCH_VIEW_ID, BREAKPOINTS_VIEW_ID, LOADED_SCRIPTS_VIEW_ID, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_CALLSTACK_ITEM_TYPE, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_DEBUG_UX, BREAKPOINT_EDITOR_CONTRIBUTION_ID, REPL_VIEW_ID, CONTEXT_BREAKPOINTS_EXIST, EDITOR_CONTRIBUTION_ID, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_SET_VARIABLE_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT, getStateLabel, State, CONTEXT_WATCH_ITEM_TYPE, CONTEXT_STACK_FRAME_SUPPORTS_RESTART, CONTEXT_BREAK_WHEN_VALUE_IS_READ_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_ACCESSED_SUPPORTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED, DISASSEMBLY_VIEW_ID, CONTEXT_SET_EXPRESSION_SUPPORTED, CONTEXT_VARIABLE_IS_READONLY, CONTEXT_CAN_VIEW_MEMORY, CONTEXT_SUSPEND_DEBUGGEE_SUPPORTED, } from 'vs/workbench/contrib/debug/common/debug'; import { DebugToolBar } from 'vs/workbench/contrib/debug/browser/debugToolBar'; import { DebugService } from 'vs/workbench/contrib/debug/browser/debugService'; -import { ADD_CONFIGURATION_ID, TOGGLE_INLINE_BREAKPOINT_ID, COPY_STACK_TRACE_ID, RESTART_SESSION_ID, TERMINATE_THREAD_ID, STEP_OVER_ID, STEP_INTO_ID, STEP_OUT_ID, PAUSE_ID, DISCONNECT_ID, STOP_ID, RESTART_FRAME_ID, CONTINUE_ID, FOCUS_REPL_ID, JUMP_TO_CURSOR_ID, RESTART_LABEL, STEP_INTO_LABEL, STEP_OVER_LABEL, STEP_OUT_LABEL, PAUSE_LABEL, DISCONNECT_LABEL, STOP_LABEL, CONTINUE_LABEL, DEBUG_START_LABEL, DEBUG_START_COMMAND_ID, DEBUG_RUN_LABEL, DEBUG_RUN_COMMAND_ID, EDIT_EXPRESSION_COMMAND_ID, REMOVE_EXPRESSION_COMMAND_ID, SELECT_AND_START_ID, SELECT_AND_START_LABEL, SET_EXPRESSION_COMMAND_ID } from 'vs/workbench/contrib/debug/browser/debugCommands'; +import { ADD_CONFIGURATION_ID, TOGGLE_INLINE_BREAKPOINT_ID, COPY_STACK_TRACE_ID, RESTART_SESSION_ID, TERMINATE_THREAD_ID, STEP_OVER_ID, STEP_INTO_ID, STEP_OUT_ID, PAUSE_ID, DISCONNECT_ID, STOP_ID, RESTART_FRAME_ID, CONTINUE_ID, FOCUS_REPL_ID, JUMP_TO_CURSOR_ID, RESTART_LABEL, STEP_INTO_LABEL, STEP_OVER_LABEL, STEP_OUT_LABEL, PAUSE_LABEL, DISCONNECT_LABEL, STOP_LABEL, CONTINUE_LABEL, DEBUG_START_LABEL, DEBUG_START_COMMAND_ID, DEBUG_RUN_LABEL, DEBUG_RUN_COMMAND_ID, EDIT_EXPRESSION_COMMAND_ID, REMOVE_EXPRESSION_COMMAND_ID, SELECT_AND_START_ID, SELECT_AND_START_LABEL, SET_EXPRESSION_COMMAND_ID, DISCONNECT_AND_SUSPEND_ID, DISCONNECT_AND_SUSPEND_LABEL } from 'vs/workbench/contrib/debug/browser/debugCommands'; import { StatusBarColorProvider } from 'vs/workbench/contrib/debug/browser/statusbarColorProvider'; import { IViewsRegistry, Extensions as ViewExtensions, IViewContainersRegistry, ViewContainerLocation, ViewContainer } from 'vs/workbench/common/views'; import { isMacintosh, isWeb } from 'vs/base/common/platform'; @@ -30,7 +30,7 @@ import { DebugStatusContribution } from 'vs/workbench/contrib/debug/browser/debu import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { launchSchemaId } from 'vs/workbench/services/configuration/common/configuration'; import { LoadedScriptsView } from 'vs/workbench/contrib/debug/browser/loadedScriptsView'; -import { RunToCursorAction } from 'vs/workbench/contrib/debug/browser/debugEditorActions'; +import { RunToCursorAction, SelectionToReplAction, SelectionToWatchExpressionsAction } from 'vs/workbench/contrib/debug/browser/debugEditorActions'; import { WatchExpressionsView, ADD_WATCH_LABEL, REMOVE_WATCH_EXPRESSIONS_COMMAND_ID, REMOVE_WATCH_EXPRESSIONS_LABEL, ADD_WATCH_ID } from 'vs/workbench/contrib/debug/browser/watchExpressionsView'; import { VariablesView, SET_VARIABLE_ID, COPY_VALUE_ID, BREAK_WHEN_VALUE_CHANGES_ID, COPY_EVALUATE_PATH_ID, ADD_TO_WATCH_ID, BREAK_WHEN_VALUE_IS_ACCESSED_ID, BREAK_WHEN_VALUE_IS_READ_ID, VIEW_MEMORY_ID } from 'vs/workbench/contrib/debug/browser/variablesView'; import { Repl } from 'vs/workbench/contrib/debug/browser/repl'; @@ -56,6 +56,7 @@ import { EditorPaneDescriptor, IEditorPaneRegistry } from 'vs/workbench/browser/ import { DisassemblyViewInput } from 'vs/workbench/contrib/debug/common/disassemblyViewInput'; import { DebugLifecycle } from 'vs/workbench/contrib/debug/common/debugLifecycle'; import { Icon } from 'vs/platform/action/common/action'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; const debugCategory = nls.localize('debugCategory', "Debug"); registerColors(); @@ -106,12 +107,15 @@ registerDebugCommandPaletteItem(STEP_INTO_ID, STEP_INTO_LABEL, CONTEXT_IN_DEBUG_ registerDebugCommandPaletteItem(STEP_OUT_ID, STEP_OUT_LABEL, CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); registerDebugCommandPaletteItem(PAUSE_ID, PAUSE_LABEL, CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE.isEqualTo('running')); registerDebugCommandPaletteItem(DISCONNECT_ID, DISCONNECT_LABEL, CONTEXT_IN_DEBUG_MODE, ContextKeyExpr.or(CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED)); +registerDebugCommandPaletteItem(DISCONNECT_AND_SUSPEND_ID, DISCONNECT_AND_SUSPEND_LABEL, CONTEXT_IN_DEBUG_MODE, ContextKeyExpr.or(CONTEXT_FOCUSED_SESSION_IS_ATTACH, ContextKeyExpr.and(CONTEXT_SUSPEND_DEBUGGEE_SUPPORTED, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED))); registerDebugCommandPaletteItem(STOP_ID, STOP_LABEL, CONTEXT_IN_DEBUG_MODE, ContextKeyExpr.or(CONTEXT_FOCUSED_SESSION_IS_ATTACH.toNegated(), CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED)); registerDebugCommandPaletteItem(CONTINUE_ID, CONTINUE_LABEL, CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); registerDebugCommandPaletteItem(FOCUS_REPL_ID, nls.localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'debugFocusConsole' }, 'Focus on Debug Console View')); registerDebugCommandPaletteItem(JUMP_TO_CURSOR_ID, nls.localize('jumpToCursor', "Jump to Cursor"), CONTEXT_JUMP_TO_CURSOR_SUPPORTED); registerDebugCommandPaletteItem(JUMP_TO_CURSOR_ID, nls.localize('SetNextStatement', "Set Next Statement"), CONTEXT_JUMP_TO_CURSOR_SUPPORTED); registerDebugCommandPaletteItem(RunToCursorAction.ID, RunToCursorAction.LABEL, ContextKeyExpr.and(CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE.isEqualTo('stopped'))); +registerDebugCommandPaletteItem(SelectionToReplAction.ID, SelectionToReplAction.LABEL, ContextKeyExpr.and(EditorContextKeys.hasNonEmptySelection, CONTEXT_IN_DEBUG_MODE)); +registerDebugCommandPaletteItem(SelectionToWatchExpressionsAction.ID, SelectionToWatchExpressionsAction.LABEL, ContextKeyExpr.and(EditorContextKeys.hasNonEmptySelection, CONTEXT_IN_DEBUG_MODE)); registerDebugCommandPaletteItem(TOGGLE_INLINE_BREAKPOINT_ID, nls.localize('inlineBreakpoint', "Inline Breakpoint")); registerDebugCommandPaletteItem(DEBUG_START_COMMAND_ID, DEBUG_START_LABEL, ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DEBUG_STATE.notEqualsTo(getStateLabel(State.Initializing)))); registerDebugCommandPaletteItem(DEBUG_RUN_COMMAND_ID, DEBUG_RUN_LABEL, ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DEBUG_STATE.notEqualsTo(getStateLabel(State.Initializing)))); @@ -135,6 +139,7 @@ const registerDebugViewMenuItem = (menuId: MenuId, id: string, title: string, or }; registerDebugViewMenuItem(MenuId.DebugCallStackContext, RESTART_SESSION_ID, RESTART_LABEL, 10, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('session'), undefined, '3_modification'); registerDebugViewMenuItem(MenuId.DebugCallStackContext, DISCONNECT_ID, DISCONNECT_LABEL, 20, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('session'), undefined, '3_modification'); +registerDebugViewMenuItem(MenuId.DebugCallStackContext, DISCONNECT_AND_SUSPEND_ID, DISCONNECT_AND_SUSPEND_LABEL, 21, ContextKeyExpr.and(CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('session'), CONTEXT_SUSPEND_DEBUGGEE_SUPPORTED, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED), undefined, '3_modification'); registerDebugViewMenuItem(MenuId.DebugCallStackContext, STOP_ID, STOP_LABEL, 30, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('session'), undefined, '3_modification'); registerDebugViewMenuItem(MenuId.DebugCallStackContext, PAUSE_ID, PAUSE_LABEL, 10, ContextKeyExpr.and(CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('thread'), CONTEXT_DEBUG_STATE.isEqualTo('running'))); registerDebugViewMenuItem(MenuId.DebugCallStackContext, CONTINUE_ID, CONTINUE_LABEL, 10, ContextKeyExpr.and(CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('thread'), CONTEXT_DEBUG_STATE.isEqualTo('stopped'))); @@ -418,8 +423,8 @@ configurationRegistry.registerConfiguration({ default: false }, 'debug.inlineValues': { - type: ['boolean', 'string'], - 'enum': [true, false, 'auto'], + type: 'string', + 'enum': ['on', 'off', 'auto'], description: nls.localize({ comment: ['This is the description for a setting'], key: 'inlineValues' }, "Show variable values inline in editor while debugging."), 'enumDescriptions': [ nls.localize('inlineValues.on', 'Always show variable values inline in editor while debugging.'), @@ -506,6 +511,11 @@ configurationRegistry.registerConfiguration({ description: nls.localize('debug.focusWindowOnBreak', "Controls whether the workbench window should be focused when the debugger breaks."), default: true }, + 'debug.focusEditorOnBreak': { + type: 'boolean', + description: nls.localize('debug.focusEditorOnBreak', "Controls whether the editor should be focused when the debugger breaks."), + default: true + }, 'debug.onTaskErrors': { enum: ['debugAnyway', 'showErrors', 'prompt', 'abort'], enumDescriptions: [nls.localize('debugAnyway', "Ignore task errors and start debugging."), nls.localize('showErrors', "Show the Problems view and do not start debugging."), nls.localize('prompt', "Prompt user."), nls.localize('cancel', "Cancel debugging.")], @@ -548,5 +558,10 @@ configurationRegistry.registerConfiguration({ default: true, description: nls.localize('debug.disassemblyView.showSourceCode', "Show Source Code in Disassembly View.") }, + 'debug.autoExpandLazyVariables': { + type: 'boolean', + default: false, + description: nls.localize('debug.autoExpandLazyVariables', "Automatically show values for variables that are lazily resolved by the debugger, such as getters.") + } } }); diff --git a/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts b/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts index 2a01435a6f5..24c64bc9f81 100644 --- a/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts +++ b/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts @@ -10,8 +10,8 @@ import Severity from 'vs/base/common/severity'; import * as strings from 'vs/base/common/strings'; import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorModel } from 'vs/editor/common/editorCommon'; -import { ITextModel } from 'vs/editor/common/model'; import { ILanguageService } from 'vs/editor/common/languages/language'; +import { ITextModel } from 'vs/editor/common/model'; import * as nls from 'vs/nls'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -22,6 +22,7 @@ import { Extensions as JSONExtensions, IJSONContributionRegistry } from 'vs/plat import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { Breakpoints } from 'vs/workbench/contrib/debug/common/breakpoints'; import { CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DEBUG_EXTENSION_AVAILABLE, IAdapterDescriptor, IAdapterManager, IConfig, IDebugAdapter, IDebugAdapterDescriptorFactory, IDebugAdapterFactory, IDebugConfiguration, IDebugSession, INTERNAL_CONSOLE_OPTIONS_SCHEMA } from 'vs/workbench/contrib/debug/common/debug'; import { Debugger } from 'vs/workbench/contrib/debug/common/debugger'; import { breakpointsExtPoint, debuggersExtPoint, launchSchema, presentationSchema } from 'vs/workbench/contrib/debug/common/debugSchemas'; @@ -42,7 +43,7 @@ export class AdapterManager extends Disposable implements IAdapterManager { private debugExtensionsAvailable: IContextKey<boolean>; private readonly _onDidRegisterDebugger = new Emitter<void>(); private readonly _onDidDebuggersExtPointRead = new Emitter<void>(); - private breakpointLanguageIdsSet = new Set<string>(); + private breakpointContributions: Breakpoints[] = []; private debuggerWhenKeys = new Set<string>(); constructor( @@ -116,13 +117,8 @@ export class AdapterManager extends Disposable implements IAdapterManager { this._onDidDebuggersExtPointRead.fire(); }); - breakpointsExtPoint.setHandler((extensions, delta) => { - delta.removed.forEach(removed => { - removed.value.forEach(breakpoints => this.breakpointLanguageIdsSet.delete(breakpoints.language)); - }); - delta.added.forEach(added => { - added.value.forEach(breakpoints => this.breakpointLanguageIdsSet.add(breakpoints.language)); - }); + breakpointsExtPoint.setHandler(extensions => { + this.breakpointContributions = extensions.flatMap(ext => ext.value.map(breakpoint => this.instantiationService.createInstance(Breakpoints, breakpoint))); }); } @@ -281,7 +277,7 @@ export class AdapterManager extends Disposable implements IAdapterManager { return true; } - return this.breakpointLanguageIdsSet.has(languageId); + return this.breakpointContributions.some(breakpoints => breakpoints.language === languageId && breakpoints.enabled); } getDebugger(type: string): Debugger | undefined { diff --git a/src/vs/workbench/contrib/debug/browser/debugColors.ts b/src/vs/workbench/contrib/debug/browser/debugColors.ts index affb86d4e98..3ebe8126973 100644 --- a/src/vs/workbench/contrib/debug/browser/debugColors.ts +++ b/src/vs/workbench/contrib/debug/browser/debugColors.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { registerColor, foreground, editorInfoForeground, editorWarningForeground, errorForeground, badgeBackground, badgeForeground, listDeemphasizedForeground, contrastBorder, inputBorder } from 'vs/platform/theme/common/colorRegistry'; +import { registerColor, foreground, editorInfoForeground, editorWarningForeground, errorForeground, badgeBackground, badgeForeground, listDeemphasizedForeground, contrastBorder, inputBorder, toolbarHoverBackground } from 'vs/platform/theme/common/colorRegistry'; import { registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { Color } from 'vs/base/common/color'; import { localize } from 'vs/nls'; @@ -125,6 +125,7 @@ export function registerColors() { const debugViewStateLabelForegroundColor = theme.getColor(debugViewStateLabelForeground)!; const debugViewStateLabelBackgroundColor = theme.getColor(debugViewStateLabelBackground)!; const debugViewValueChangedHighlightColor = theme.getColor(debugViewValueChangedHighlight)!; + const toolbarHoverBackgroundColor = theme.getColor(toolbarHoverBackground); collector.addRule(` /* Text colour of the call stack row's filename */ @@ -182,6 +183,10 @@ export function registerColors() { animation-duration: 1s; animation-fill-mode: forwards; } + + .monaco-list-row .expression .lazy-button:hover { + background-color: ${toolbarHoverBackgroundColor} + } `); const contrastBorderColor = theme.getColor(contrastBorder); diff --git a/src/vs/workbench/contrib/debug/browser/debugCommands.ts b/src/vs/workbench/contrib/debug/browser/debugCommands.ts index 4a022554117..3042792f8e8 100644 --- a/src/vs/workbench/contrib/debug/browser/debugCommands.ts +++ b/src/vs/workbench/contrib/debug/browser/debugCommands.ts @@ -44,6 +44,7 @@ export const STEP_INTO_ID = 'workbench.action.debug.stepInto'; export const STEP_OUT_ID = 'workbench.action.debug.stepOut'; export const PAUSE_ID = 'workbench.action.debug.pause'; export const DISCONNECT_ID = 'workbench.action.debug.disconnect'; +export const DISCONNECT_AND_SUSPEND_ID = 'workbench.action.debug.disconnectAndSuspend'; export const STOP_ID = 'workbench.action.debug.stop'; export const RESTART_FRAME_ID = 'workbench.action.debug.restartFrame'; export const CONTINUE_ID = 'workbench.action.debug.continue'; @@ -64,6 +65,7 @@ export const STEP_INTO_LABEL = nls.localize('stepIntoDebug', "Step Into"); export const STEP_OUT_LABEL = nls.localize('stepOutDebug', "Step Out"); export const PAUSE_LABEL = nls.localize('pauseDebug', "Pause"); export const DISCONNECT_LABEL = nls.localize('disconnect', "Disconnect"); +export const DISCONNECT_AND_SUSPEND_LABEL = nls.localize('disconnectSuspend', "Disconnect and Suspend"); export const STOP_LABEL = nls.localize('stop', "Stop"); export const CONTINUE_LABEL = nls.localize('continueDebug', "Continue"); export const FOCUS_SESSION_LABEL = nls.localize('focusSession', "Focus Session"); @@ -313,7 +315,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } }); -async function stopHandler(accessor: ServicesAccessor, _: string, context: CallStackContext | unknown, disconnect: boolean): Promise<void> { +async function stopHandler(accessor: ServicesAccessor, _: string, context: CallStackContext | unknown, disconnect: boolean, suspend?: boolean): Promise<void> { const debugService = accessor.get(IDebugService); let session: IDebugSession | undefined; if (isSessionContext(context)) { @@ -329,7 +331,7 @@ async function stopHandler(accessor: ServicesAccessor, _: string, context: CallS session = session.parentSession; } - await debugService.stopSession(session, disconnect); + await debugService.stopSession(session, disconnect, suspend); } KeybindingsRegistry.registerCommandAndKeybindingRule({ @@ -340,6 +342,11 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ handler: (accessor, _, context) => stopHandler(accessor, _, context, true) }); +CommandsRegistry.registerCommand({ + id: DISCONNECT_AND_SUSPEND_ID, + handler: (accessor, _, context) => stopHandler(accessor, _, context, true, true) +}); + KeybindingsRegistry.registerCommandAndKeybindingRule({ id: STOP_ID, weight: KeybindingWeight.WorkbenchContrib, @@ -399,7 +406,7 @@ CommandsRegistry.registerCommand({ if (stoppedChildSession && session.state !== State.Stopped) { session = stoppedChildSession; } - await debugService.focusStackFrame(undefined, undefined, session, true); + await debugService.focusStackFrame(undefined, undefined, session, { explicit: true }); const stackFrame = debugService.getViewModel().focusedStackFrame; if (stackFrame) { await stackFrame.openInEditor(editorService, true); diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts b/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts index bcb3748c6a1..4264520e56a 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts @@ -234,13 +234,16 @@ export class RunToCursorAction extends EditorAction { } } -class SelectionToReplAction extends EditorAction { +export class SelectionToReplAction extends EditorAction { + + public static readonly ID = 'editor.debug.action.selectionToRepl'; + public static readonly LABEL = nls.localize('evaluateInDebugConsole', "Evaluate in Debug Console"); constructor() { super({ - id: 'editor.debug.action.selectionToRepl', - label: nls.localize('evaluateInDebugConsole', "Evaluate in Debug Console"), - alias: 'Evaluate', + id: SelectionToReplAction.ID, + label: SelectionToReplAction.LABEL, + alias: 'Debug: Evaluate in Console', precondition: ContextKeyExpr.and(EditorContextKeys.hasNonEmptySelection, CONTEXT_IN_DEBUG_MODE, EditorContextKeys.editorTextFocus), contextMenuOpts: { group: 'debug', @@ -264,13 +267,16 @@ class SelectionToReplAction extends EditorAction { } } -class SelectionToWatchExpressionsAction extends EditorAction { +export class SelectionToWatchExpressionsAction extends EditorAction { + + public static readonly ID = 'editor.debug.action.selectionToWatch'; + public static readonly LABEL = nls.localize('addToWatch', "Add to Watch"); constructor() { super({ - id: 'editor.debug.action.selectionToWatch', - label: nls.localize('addToWatch', "Add to Watch"), - alias: 'Add to Watch', + id: SelectionToWatchExpressionsAction.ID, + label: SelectionToWatchExpressionsAction.LABEL, + alias: 'Debug: Add to Watch', precondition: ContextKeyExpr.and(EditorContextKeys.hasNonEmptySelection, CONTEXT_IN_DEBUG_MODE, EditorContextKeys.editorTextFocus), contextMenuOpts: { group: 'debug', diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts index 81133f8fade..848242efad6 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts @@ -179,8 +179,8 @@ function getWordToLineNumbersMap(model: ITextModel | null): Map<string, number[] continue; } - model.forceTokenization(lineNumber); - const lineTokens = model.getLineTokens(lineNumber); + model.tokenization.forceTokenization(lineNumber); + const lineTokens = model.tokenization.getLineTokens(lineNumber); for (let tokenIndex = 0, tokenCount = lineTokens.getCount(); tokenIndex < tokenCount; tokenIndex++) { const tokenType = lineTokens.getStandardTokenType(tokenIndex); @@ -627,7 +627,7 @@ export class DebugEditorContribution implements IDebugEditorContribution { const model = this.editor.getModel(); const inlineValuesSetting = this.configurationService.getValue<IDebugConfiguration>('debug').inlineValues; - const inlineValuesTurnedOn = inlineValuesSetting === true || (inlineValuesSetting === 'auto' && model && this.languageFeaturesService.inlineValuesProvider.has(model)); + const inlineValuesTurnedOn = inlineValuesSetting === true || inlineValuesSetting === 'on' || (inlineValuesSetting === 'auto' && model && this.languageFeaturesService.inlineValuesProvider.has(model)); if (!inlineValuesTurnedOn || !model || !stackFrame || model.uri.toString() !== stackFrame.source.uri.toString()) { if (!this.removeInlineValuesScheduler.isScheduled()) { this.removeInlineValuesScheduler.schedule(); diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index 39b73ddbc29..224b72fe4fa 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -801,9 +801,9 @@ export class DebugService implements IDebugService { }); } - async stopSession(session: IDebugSession | undefined, disconnect = false): Promise<any> { + async stopSession(session: IDebugSession | undefined, disconnect = false, suspend = false): Promise<any> { if (session) { - return disconnect ? session.disconnect() : session.terminate(); + return disconnect ? session.disconnect(undefined, suspend) : session.terminate(); } const sessions = this.model.getSessions(); @@ -815,7 +815,7 @@ export class DebugService implements IDebugService { this.cancelTokens(undefined); } - return Promise.all(sessions.map(s => disconnect ? s.disconnect() : s.terminate())); + return Promise.all(sessions.map(s => disconnect ? s.disconnect(undefined, suspend) : s.terminate())); } private async substituteVariables(launch: ILaunch | undefined, config: IConfig): Promise<IConfig | undefined> { @@ -852,11 +852,11 @@ export class DebugService implements IDebugService { //---- focus management - async focusStackFrame(_stackFrame: IStackFrame | undefined, _thread?: IThread, _session?: IDebugSession, explicit?: boolean): Promise<void> { + async focusStackFrame(_stackFrame: IStackFrame | undefined, _thread?: IThread, _session?: IDebugSession, options?: { explicit?: boolean; preserveFocus?: boolean; sideBySide?: boolean; pinned?: boolean }): Promise<void> { const { stackFrame, thread, session } = getStackFrameThreadAndSessionToFocus(this.model, _stackFrame, _thread, _session); if (stackFrame) { - const editor = await stackFrame.openInEditor(this.editorService, true); + const editor = await stackFrame.openInEditor(this.editorService, options?.preserveFocus ?? true, options?.sideBySide, options?.pinned); if (editor) { if (editor.input === DisassemblyViewInput.instance) { // Go to address is invoked via setFocus @@ -880,7 +880,7 @@ export class DebugService implements IDebugService { this.debugType.reset(); } - this.viewModel.setFocus(stackFrame, thread, session, !!explicit); + this.viewModel.setFocus(stackFrame, thread, session, !!options?.explicit); } //---- watches diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index 6d7ec022e42..3ff60d94d24 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -172,6 +172,11 @@ export class DebugSession implements IDebugSession { return this._options.debugUI?.simple ?? false; } + get autoExpandLazyVariables(): boolean { + // This tiny helper avoids converting the entire debug model to use service injection + return this.configurationService.getValue<IDebugConfiguration>('debug').autoExpandLazyVariables; + } + setConfiguration(configuration: { resolved: IConfig; unresolved: IConfig | undefined }) { this._configuration = configuration; } @@ -350,7 +355,7 @@ export class DebugSession implements IDebugSession { /** * end the current debug adapter session */ - async disconnect(restart = false): Promise<void> { + async disconnect(restart = false, suspend = false): Promise<void> { if (!this.raw) { // Adapter went down but it did not send a 'terminated' event, simulate like the event has been sent this.onDidExitAdapter(); @@ -358,9 +363,10 @@ export class DebugSession implements IDebugSession { this.cancelAllRequests(); if (this._options.lifecycleManagedByParent && this.parentSession) { - await this.parentSession.disconnect(restart); + await this.parentSession.disconnect(restart, suspend); } else if (this.raw) { - await this.raw.disconnect({ restart, terminateDebuggee: false }); + // TODO terminateDebuggee should be undefined by default? + await this.raw.disconnect({ restart, terminateDebuggee: false, suspendDebuggee: suspend }); } if (!restart) { @@ -957,7 +963,8 @@ export class DebugSession implements IDebugSession { const focusedStackFrame = this.debugService.getViewModel().focusedStackFrame; if (!focusedStackFrame || focusedStackFrame.thread.session === this) { // Only take focus if nothing is focused, or if the focus is already on the current session - await this.debugService.focusStackFrame(undefined, thread); + const preserveFocus = !this.configurationService.getValue<IDebugConfiguration>('debug').focusEditorOnBreak; + await this.debugService.focusStackFrame(undefined, thread, undefined, { preserveFocus }); } if (thread.stoppedDetails) { @@ -1003,7 +1010,7 @@ export class DebugSession implements IDebugSession { this.passFocusScheduler.cancel(); if (focusedThread && event.body.threadId === focusedThread.threadId) { // De-focus the thread in case it was focused - this.debugService.focusStackFrame(undefined, undefined, viewModel.focusedSession, false); + this.debugService.focusStackFrame(undefined, undefined, viewModel.focusedSession, { explicit: false }); } } })); @@ -1070,7 +1077,7 @@ export class DebugSession implements IDebugSession { // only log telemetry events from debug adapter if the debug extension provided the telemetry key // and the user opted in telemetry const telemetryEndpoint = this.raw.dbgr.getCustomTelemetryEndpoint(); - if (telemetryEndpoint && this.telemetryService.telemetryLevel !== TelemetryLevel.NONE) { + if (telemetryEndpoint && this.telemetryService.telemetryLevel.value !== TelemetryLevel.NONE) { // __GDPR__TODO__ We're sending events in the name of the debug extension and we can not ensure that those are declared correctly. let data = event.body.data; if (!telemetryEndpoint.sendErrorTelemetry && event.body.data) { diff --git a/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts b/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts index 4c0b2af2ea0..4667fc38a3e 100644 --- a/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts +++ b/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts @@ -15,9 +15,12 @@ import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { withUndefinedAsNull } from 'vs/base/common/types'; import { IMarkerService, MarkerSeverity } from 'vs/platform/markers/common/markers'; import { IDebugConfiguration } from 'vs/workbench/contrib/debug/common/debug'; -import { createErrorWithActions } from 'vs/base/common/errorMessage'; import { IViewsService } from 'vs/workbench/common/views'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { createErrorWithActions } from 'vs/base/common/errorMessage'; +import { Action } from 'vs/base/common/actions'; +import { DEBUG_CONFIGURE_COMMAND_ID, DEBUG_CONFIGURE_LABEL } from 'vs/workbench/contrib/debug/browser/debugCommands'; +import { ICommandService } from 'vs/platform/commands/common/commands'; function once(match: (e: TaskEvent) => boolean, event: Event<TaskEvent>): Event<TaskEvent> { return (listener, thisArgs = null, disposables?) => { @@ -48,7 +51,8 @@ export class DebugTaskRunner { @IConfigurationService private readonly configurationService: IConfigurationService, @IViewsService private readonly viewsService: IViewsService, @IDialogService private readonly dialogService: IDialogService, - @IStorageService private readonly storageService: IStorageService + @IStorageService private readonly storageService: IStorageService, + @ICommandService private readonly commandService: ICommandService ) { } cancel(): void { @@ -158,7 +162,7 @@ export class DebugTaskRunner { const errorMessage = typeof taskId === 'string' ? nls.localize('DebugTaskNotFoundWithTaskId', "Could not find the task '{0}'.", taskId) : nls.localize('DebugTaskNotFound', "Could not find the specified task."); - return Promise.reject(createErrorWithActions(errorMessage)); + return Promise.reject(createErrorWithActions(errorMessage, [new Action(DEBUG_CONFIGURE_COMMAND_ID, DEBUG_CONFIGURE_LABEL, undefined, true, () => this.commandService.executeCommand(DEBUG_CONFIGURE_COMMAND_ID))])); } // If a task is missing the problem matcher the promise will never complete, so we need to have a workaround #35340 diff --git a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts index 8bb67ae408f..13d089466e4 100644 --- a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts +++ b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts @@ -3,36 +3,39 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./media/debugToolBar'; -import * as errors from 'vs/base/common/errors'; import * as browser from 'vs/base/browser/browser'; import * as dom from 'vs/base/browser/dom'; +import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; +import { ActionBar, ActionsOrientation, IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; +import { Action, IAction, IRunEvent, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions'; import * as arrays from 'vs/base/common/arrays'; +import { RunOnceScheduler } from 'vs/base/common/async'; +import * as errors from 'vs/base/common/errors'; +import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import 'vs/css!./media/debugToolBar'; +import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { localize } from 'vs/nls'; -import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; -import { IAction, IRunEvent, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from 'vs/base/common/actions'; -import { ActionBar, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; -import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; -import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { IDebugConfiguration, IDebugService, State, CONTEXT_DEBUG_STATE, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_MULTI_SESSION_DEBUG, VIEWLET_ID } from 'vs/workbench/contrib/debug/common/debug'; -import { FocusSessionActionViewItem } from 'vs/workbench/contrib/debug/browser/debugActionViewItems'; +import { ICommandAction } from 'vs/platform/action/common/action'; +import { DropdownWithPrimaryActionViewItem } from 'vs/platform/actions/browser/dropdownWithPrimaryActionViewItem'; +import { createActionViewItem, createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IMenu, IMenuService, MenuId, MenuItemAction, MenuRegistry } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ContextKeyExpr, ContextKeyExpression, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { INotificationService } from 'vs/platform/notification/common/notification'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IThemeService, Themable, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { contrastBorder, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { RunOnceScheduler } from 'vs/base/common/async'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { createActionViewItem, createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { IMenu, IMenuService, MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; -import { IContextKeyService, ContextKeyExpression, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import * as icons from 'vs/workbench/contrib/debug/browser/debugIcons'; +import { IThemeService, Themable, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { FocusSessionActionViewItem } from 'vs/workbench/contrib/debug/browser/debugActionViewItems'; import { debugToolBarBackground, debugToolBarBorder } from 'vs/workbench/contrib/debug/browser/debugColors'; -import { URI } from 'vs/base/common/uri'; -import { CONTINUE_LABEL, CONTINUE_ID, PAUSE_ID, STOP_ID, DISCONNECT_ID, STEP_OVER_ID, STEP_INTO_ID, RESTART_SESSION_ID, STEP_OUT_ID, STEP_BACK_ID, REVERSE_CONTINUE_ID, RESTART_LABEL, STEP_OUT_LABEL, STEP_INTO_LABEL, STEP_OVER_LABEL, DISCONNECT_LABEL, STOP_LABEL, PAUSE_LABEL, FOCUS_SESSION_ID, FOCUS_SESSION_LABEL } from 'vs/workbench/contrib/debug/browser/debugCommands'; -import { ICommandAction } from 'vs/platform/action/common/action'; +import { CONTINUE_ID, CONTINUE_LABEL, DISCONNECT_AND_SUSPEND_ID, DISCONNECT_AND_SUSPEND_LABEL, DISCONNECT_ID, DISCONNECT_LABEL, FOCUS_SESSION_ID, FOCUS_SESSION_LABEL, PAUSE_ID, PAUSE_LABEL, RESTART_LABEL, RESTART_SESSION_ID, REVERSE_CONTINUE_ID, STEP_BACK_ID, STEP_INTO_ID, STEP_INTO_LABEL, STEP_OUT_ID, STEP_OUT_LABEL, STEP_OVER_ID, STEP_OVER_LABEL, STOP_ID, STOP_LABEL } from 'vs/workbench/contrib/debug/browser/debugCommands'; +import * as icons from 'vs/workbench/contrib/debug/browser/debugIcons'; +import { CONTEXT_DEBUG_STATE, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_MULTI_SESSION_DEBUG, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_SUSPEND_DEBUGGEE_SUPPORTED, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED, IDebugConfiguration, IDebugService, State, VIEWLET_ID } from 'vs/workbench/contrib/debug/common/debug'; +import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; const DEBUG_TOOLBAR_POSITION_KEY = 'debug.actionswidgetposition'; const DEBUG_TOOLBAR_Y_KEY = 'debug.actionswidgety'; @@ -51,6 +54,8 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { private isVisible = false; private isBuilt = false; + private readonly stopActionViewItemDisposables = this._register(new DisposableStore()); + constructor( @INotificationService private readonly notificationService: INotificationService, @ITelemetryService private readonly telemetryService: ITelemetryService, @@ -61,7 +66,7 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { @IThemeService themeService: IThemeService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IMenuService menuService: IMenuService, - @IContextKeyService contextKeyService: IContextKeyService + @IContextKeyService contextKeyService: IContextKeyService, ) { super(themeService); @@ -80,7 +85,14 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { actionViewItemProvider: (action: IAction) => { if (action.id === FOCUS_SESSION_ID) { return this.instantiationService.createInstance(FocusSessionActionViewItem, action, undefined); + } else if (action.id === STOP_ID || action.id === DISCONNECT_ID) { + this.stopActionViewItemDisposables.clear(); + const item = this.instantiationService.invokeFunction(accessor => createDisconnectMenuItemAction(action as MenuItemAction, this.stopActionViewItemDisposables, accessor)); + if (item) { + return item; + } } + return createActionViewItem(this.instantiationService, action); } })); @@ -256,6 +268,31 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { } } +export function createDisconnectMenuItemAction(action: MenuItemAction, disposables: DisposableStore, accessor: ServicesAccessor): IActionViewItem | undefined { + const menuService = accessor.get(IMenuService); + const contextKeyService = accessor.get(IContextKeyService); + const instantiationService = accessor.get(IInstantiationService); + const contextMenuService = accessor.get(IContextMenuService); + + const menu = menuService.createMenu(MenuId.DebugToolBarStop, contextKeyService); + const secondary: IAction[] = []; + disposables.add(createAndFillInActionBarActions(menu, { shouldForwardArgs: true }, secondary)); + + if (!secondary.length) { + return undefined; + } + + const dropdownAction = disposables.add(new Action('notebook.moreRunActions', localize('notebook.moreRunActionsLabel', "More..."), 'codicon-chevron-down', true)); + const item = instantiationService.createInstance(DropdownWithPrimaryActionViewItem, + action as MenuItemAction, + dropdownAction, + secondary, + 'debug-stop-actions', + contextMenuService, + {}); + return item; +} + // Debug toolbar const debugViewTitleItems: IDisposable[] = []; @@ -303,8 +340,8 @@ MenuRegistry.onDidChangeMenu(e => { registerDebugToolBarItem(CONTINUE_ID, CONTINUE_LABEL, 10, icons.debugContinue, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); registerDebugToolBarItem(PAUSE_ID, PAUSE_LABEL, 10, icons.debugPause, CONTEXT_DEBUG_STATE.notEqualsTo('stopped'), CONTEXT_DEBUG_STATE.isEqualTo('running')); -registerDebugToolBarItem(STOP_ID, STOP_LABEL, 70, icons.debugStop, CONTEXT_FOCUSED_SESSION_IS_ATTACH.toNegated(), undefined, { id: DISCONNECT_ID, title: DISCONNECT_LABEL, icon: icons.debugDisconnect }); -registerDebugToolBarItem(DISCONNECT_ID, DISCONNECT_LABEL, 70, icons.debugDisconnect, CONTEXT_FOCUSED_SESSION_IS_ATTACH, undefined, { id: STOP_ID, title: STOP_LABEL, icon: icons.debugStop }); +registerDebugToolBarItem(STOP_ID, STOP_LABEL, 70, icons.debugStop, CONTEXT_FOCUSED_SESSION_IS_ATTACH.toNegated(), undefined, { id: DISCONNECT_ID, title: DISCONNECT_LABEL, icon: icons.debugDisconnect, precondition: ContextKeyExpr.and(CONTEXT_FOCUSED_SESSION_IS_ATTACH.toNegated(), CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED), }); +registerDebugToolBarItem(DISCONNECT_ID, DISCONNECT_LABEL, 70, icons.debugDisconnect, CONTEXT_FOCUSED_SESSION_IS_ATTACH, undefined, { id: STOP_ID, title: STOP_LABEL, icon: icons.debugStop, precondition: ContextKeyExpr.and(CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED), }); registerDebugToolBarItem(STEP_OVER_ID, STEP_OVER_LABEL, 20, icons.debugStepOver, undefined, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); registerDebugToolBarItem(STEP_INTO_ID, STEP_INTO_LABEL, 30, icons.debugStepInto, undefined, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); registerDebugToolBarItem(STEP_OUT_ID, STEP_OUT_LABEL, 40, icons.debugStepOut, undefined, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); @@ -312,3 +349,39 @@ registerDebugToolBarItem(RESTART_SESSION_ID, RESTART_LABEL, 60, icons.debugResta registerDebugToolBarItem(STEP_BACK_ID, localize('stepBackDebug', "Step Back"), 50, icons.debugStepBack, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); registerDebugToolBarItem(REVERSE_CONTINUE_ID, localize('reverseContinue', "Reverse"), 55, icons.debugReverseContinue, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); registerDebugToolBarItem(FOCUS_SESSION_ID, FOCUS_SESSION_LABEL, 100, undefined, CONTEXT_MULTI_SESSION_DEBUG); + +MenuRegistry.appendMenuItem(MenuId.DebugToolBarStop, { + group: 'navigation', + when: ContextKeyExpr.and(CONTEXT_FOCUSED_SESSION_IS_ATTACH.toNegated(), CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED), + order: 0, + command: { + id: DISCONNECT_ID, + title: DISCONNECT_LABEL, + icon: icons.debugDisconnect + } +}); + +MenuRegistry.appendMenuItem(MenuId.DebugToolBarStop, { + group: 'navigation', + when: ContextKeyExpr.and(CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED), + order: 0, + command: { + id: STOP_ID, + title: STOP_LABEL, + icon: icons.debugStop + } +}); + +MenuRegistry.appendMenuItem(MenuId.DebugToolBarStop, { + group: 'navigation', + when: ContextKeyExpr.or( + ContextKeyExpr.and(CONTEXT_FOCUSED_SESSION_IS_ATTACH.toNegated(), CONTEXT_SUSPEND_DEBUGGEE_SUPPORTED, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED), + ContextKeyExpr.and(CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_SUSPEND_DEBUGGEE_SUPPORTED), + ), + order: 0, + command: { + id: DISCONNECT_AND_SUSPEND_ID, + title: DISCONNECT_AND_SUSPEND_LABEL, + icon: icons.debugDisconnect + } +}); diff --git a/src/vs/workbench/contrib/debug/browser/debugViewlet.ts b/src/vs/workbench/contrib/debug/browser/debugViewlet.ts index a90416994e1..ae38d059d88 100644 --- a/src/vs/workbench/contrib/debug/browser/debugViewlet.ts +++ b/src/vs/workbench/contrib/debug/browser/debugViewlet.ts @@ -3,34 +3,35 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; +import { IAction } from 'vs/base/common/actions'; +import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; import 'vs/css!./media/debugViewlet'; import * as nls from 'vs/nls'; -import { IAction } from 'vs/base/common/actions'; -import { IDebugService, VIEWLET_ID, State, BREAKPOINTS_VIEW_ID, CONTEXT_DEBUG_UX, CONTEXT_DEBUG_UX_KEY, REPL_VIEW_ID, CONTEXT_DEBUG_STATE, ILaunch, getStateLabel, CONTEXT_DEBUGGERS_AVAILABLE } from 'vs/workbench/contrib/debug/common/debug'; -import { StartDebugActionViewItem, FocusSessionActionViewItem } from 'vs/workbench/contrib/debug/browser/debugActionViewItems'; +import { createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { Action2, MenuId, MenuItemAction, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IProgressService } from 'vs/platform/progress/common/progress'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IStorageService } from 'vs/platform/storage/common/storage'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ViewPaneContainer, ViewsSubMenu } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; -import { MenuId, registerAction2, Action2, MenuRegistry } from 'vs/platform/actions/common/actions'; -import { IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { ViewPaneContainer, ViewsSubMenu } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { WorkbenchStateContext } from 'vs/workbench/common/contextkeys'; import { IViewDescriptorService, IViewsService } from 'vs/workbench/common/views'; -import { WelcomeView } from 'vs/workbench/contrib/debug/browser/welcomeView'; +import { FocusSessionActionViewItem, StartDebugActionViewItem } from 'vs/workbench/contrib/debug/browser/debugActionViewItems'; +import { DEBUG_CONFIGURE_COMMAND_ID, DEBUG_CONFIGURE_LABEL, DEBUG_START_COMMAND_ID, DEBUG_START_LABEL, DISCONNECT_ID, FOCUS_SESSION_ID, SELECT_AND_START_ID, STOP_ID } from 'vs/workbench/contrib/debug/browser/debugCommands'; import { debugConfigure } from 'vs/workbench/contrib/debug/browser/debugIcons'; -import { WorkbenchStateContext } from 'vs/workbench/common/contextkeys'; -import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; -import { FOCUS_SESSION_ID, SELECT_AND_START_ID, DEBUG_CONFIGURE_COMMAND_ID, DEBUG_CONFIGURE_LABEL, DEBUG_START_LABEL, DEBUG_START_COMMAND_ID } from 'vs/workbench/contrib/debug/browser/debugCommands'; -import { IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; +import { createDisconnectMenuItemAction } from 'vs/workbench/contrib/debug/browser/debugToolBar'; +import { WelcomeView } from 'vs/workbench/contrib/debug/browser/welcomeView'; +import { BREAKPOINTS_VIEW_ID, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DEBUG_STATE, CONTEXT_DEBUG_UX, CONTEXT_DEBUG_UX_KEY, getStateLabel, IDebugService, ILaunch, REPL_VIEW_ID, State, VIEWLET_ID } from 'vs/workbench/contrib/debug/common/debug'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; export class DebugViewPaneContainer extends ViewPaneContainer { @@ -39,6 +40,8 @@ export class DebugViewPaneContainer extends ViewPaneContainer { private breakpointView: ViewPane | undefined; private paneListeners = new Map<string, IDisposable>(); + private readonly stopActionViewItemDisposables = this._register(new DisposableStore()); + constructor( @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, @ITelemetryService telemetryService: ITelemetryService, @@ -53,7 +56,7 @@ export class DebugViewPaneContainer extends ViewPaneContainer { @IConfigurationService configurationService: IConfigurationService, @IContextViewService private readonly contextViewService: IContextViewService, @IContextKeyService private readonly contextKeyService: IContextKeyService, - @IViewDescriptorService viewDescriptorService: IViewDescriptorService + @IViewDescriptorService viewDescriptorService: IViewDescriptorService, ) { super(VIEWLET_ID, { mergeViewWithContainerWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService, viewDescriptorService); @@ -97,6 +100,15 @@ export class DebugViewPaneContainer extends ViewPaneContainer { if (action.id === FOCUS_SESSION_ID) { return new FocusSessionActionViewItem(action, undefined, this.debugService, this.themeService, this.contextViewService, this.configurationService); } + + if (action.id === STOP_ID || action.id === DISCONNECT_ID) { + this.stopActionViewItemDisposables.clear(); + const item = this.instantiationService.invokeFunction(accessor => createDisconnectMenuItemAction(action as MenuItemAction, this.stopActionViewItemDisposables, accessor)); + if (item) { + return item; + } + } + return createActionViewItem(this.instantiationService, action); } diff --git a/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css b/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css index 43865553172..577a80c532e 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css +++ b/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css @@ -84,11 +84,14 @@ font-size: 11px; } -.monaco-workbench .monaco-list-row .expression .value, -.monaco-workbench .monaco-list-row .expression .lazy-button { +.monaco-workbench .monaco-list-row .expression .value { margin-left: 6px; } +.monaco-workbench .monaco-list-row .expression .lazy-button { + margin-left: 3px; +} + /* Links */ .monaco-workbench .monaco-list-row .expression .value a.link:hover { @@ -119,20 +122,14 @@ .monaco-workbench .monaco-list-row .expression .lazy-button { display: none; -} - -.monaco-workbench .monaco-list-row .expression .lazy-button:hover { - text-decoration: underline; + border-radius: 5px; + padding: 3px; } .monaco-workbench .monaco-list-row .expression.lazy .lazy-button { display: inline; } -.monaco-workbench .monaco-list-row .expression.lazy .value { - display: none; -} - .monaco-workbench .debug-inline-value { background-color: var(--vscode-editor-inlineValuesBackground); color: var(--vscode-editor-inlineValuesForeground); diff --git a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts index d1b3b798d9f..848245e3978 100644 --- a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import { Event, Emitter } from 'vs/base/common/event'; import * as objects from 'vs/base/common/objects'; -import { Action } from 'vs/base/common/actions'; +import { toAction } from 'vs/base/common/actions'; import * as errors from 'vs/base/common/errors'; import { createErrorWithActions } from 'vs/base/common/errorMessage'; import { formatPII, isUri } from 'vs/workbench/contrib/debug/common/debugUtils'; @@ -284,7 +284,8 @@ export class RawDebugSession implements IDisposable { */ disconnect(args: DebugProtocol.DisconnectArguments): Promise<any> { const terminateDebuggee = this.capabilities.supportTerminateDebuggee ? args.terminateDebuggee : undefined; - return this.shutdown(undefined, args.restart, terminateDebuggee); + const suspendDebuggee = this.capabilities.supportTerminateDebuggee && this.capabilities.supportSuspendDebuggee ? args.suspendDebuggee : undefined; + return this.shutdown(undefined, args.restart, terminateDebuggee, suspendDebuggee); } //---- DAP requests @@ -553,12 +554,20 @@ export class RawDebugSession implements IDisposable { //---- private - private async shutdown(error?: Error, restart = false, terminateDebuggee: boolean | undefined = undefined): Promise<any> { + private async shutdown(error?: Error, restart = false, terminateDebuggee: boolean | undefined = undefined, suspendDebuggee: boolean | undefined = undefined): Promise<any> { if (!this.inShutdown) { this.inShutdown = true; if (this.debugAdapter) { try { - const args = typeof terminateDebuggee === 'boolean' ? { restart, terminateDebuggee } : { restart }; + const args: DebugProtocol.DisconnectArguments = { restart }; + if (typeof terminateDebuggee === 'boolean') { + args.terminateDebuggee = terminateDebuggee; + } + + if (typeof suspendDebuggee === 'boolean') { + args.suspendDebuggee = suspendDebuggee; + } + this.send('disconnect', args, undefined, 2000); } catch (e) { // Catch the potential 'disconnect' error - no need to show it to the user since the adapter is shutting down @@ -738,11 +747,7 @@ export class RawDebugSession implements IDisposable { const uri = URI.parse(url); // Use a suffixed id if uri invokes a command, so default 'Open launch.json' command is suppressed on dialog const actionId = uri.scheme === Schemas.command ? 'debug.moreInfo.command' : 'debug.moreInfo'; - return createErrorWithActions(userMessage, { - actions: [new Action(actionId, label, undefined, true, async () => { - this.openerService.open(uri, { allowCommands: true }); - })] - }); + return createErrorWithActions(userMessage, [toAction({ id: actionId, label, run: () => this.openerService.open(uri, { allowCommands: true }) })]); } if (showErrors && error && error.format && error.showUser) { this.notificationService.error(userMessage); diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index cc2a06f698e..d171c967d4f 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -895,7 +895,7 @@ registerAction2(class extends ViewAction<Repl> { session = stopppedChildSession; } } - await debugService.focusStackFrame(undefined, undefined, session, true); + await debugService.focusStackFrame(undefined, undefined, session, { explicit: true }); } // Need to select the session in the view since the focussed session might not have changed await view.selectSession(session); diff --git a/src/vs/workbench/contrib/debug/common/breakpoints.ts b/src/vs/workbench/contrib/debug/common/breakpoints.ts new file mode 100644 index 00000000000..38bc0d5114d --- /dev/null +++ b/src/vs/workbench/contrib/debug/common/breakpoints.ts @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ContextKeyExpr, ContextKeyExpression, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IBreakpointContribution } from 'vs/workbench/contrib/debug/common/debug'; + +export class Breakpoints { + + private breakpointsWhen: ContextKeyExpression | undefined; + + constructor( + private readonly breakpointContribution: IBreakpointContribution, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + ) { + this.breakpointsWhen = typeof breakpointContribution.when === 'string' ? ContextKeyExpr.deserialize(breakpointContribution.when) : undefined; + } + + get language(): string { + return this.breakpointContribution.language; + } + + get enabled(): boolean { + return !this.breakpointsWhen || this.contextKeyService.contextMatchesRules(this.breakpointsWhen); + } +} diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 7fd322d2d11..9457ae8c1b4 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -80,6 +80,7 @@ export const CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED = new RawContextKey<bool export const CONTEXT_BREAK_WHEN_VALUE_IS_ACCESSED_SUPPORTED = new RawContextKey<boolean>('breakWhenValueIsAccessedSupported', false, { type: 'boolean', description: nls.localize('breakWhenValueIsAccessedSupported', "True when the focused breakpoint supports to break when value is accessed.") }); export const CONTEXT_BREAK_WHEN_VALUE_IS_READ_SUPPORTED = new RawContextKey<boolean>('breakWhenValueIsReadSupported', false, { type: 'boolean', description: nls.localize('breakWhenValueIsReadSupported', "True when the focused breakpoint supports to break when value is read.") }); export const CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED = new RawContextKey<boolean>('terminateDebuggeeSupported', false, { type: 'boolean', description: nls.localize('terminateDebuggeeSupported', "True when the focused session supports the terminate debuggee capability.") }); +export const CONTEXT_SUSPEND_DEBUGGEE_SUPPORTED = new RawContextKey<boolean>('suspendDebuggeeSupported', false, { type: 'boolean', description: nls.localize('suspendDebuggeeSupported', "True when the focused session supports the suspend debuggee capability.") }); export const CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT = new RawContextKey<boolean>('variableEvaluateNamePresent', false, { type: 'boolean', description: nls.localize('variableEvaluateNamePresent', "True when the focused variable has an 'evalauteName' field set.") }); export const CONTEXT_VARIABLE_IS_READONLY = new RawContextKey<boolean>('variableIsReadonly', false, { type: 'boolean', description: nls.localize('variableIsReadonly', "True when the focused variable is readonly.") }); export const CONTEXT_EXCEPTION_WIDGET_VISIBLE = new RawContextKey<boolean>('exceptionWidgetVisible', false, { type: 'boolean', description: nls.localize('exceptionWidgetVisible', "True when the exception widget is visible.") }); @@ -290,6 +291,7 @@ export interface IDebugSession extends ITreeElement { readonly compoundRoot: DebugCompoundRoot | undefined; readonly name: string; readonly isSimpleUI: boolean; + readonly autoExpandLazyVariables: boolean; setSubId(subId: string | undefined): void; @@ -339,7 +341,7 @@ export interface IDebugSession extends ITreeElement { launchOrAttach(config: IConfig): Promise<void>; restart(): Promise<void>; terminate(restart?: boolean /* false */): Promise<void>; - disconnect(restart?: boolean /* false */): Promise<void>; + disconnect(restart?: boolean /* false */, suspend?: boolean): Promise<void>; sendBreakpoints(modelUri: uri, bpts: IBreakpoint[], sourceModified: boolean): Promise<void>; sendFunctionBreakpoints(fbps: IFunctionBreakpoint[]): Promise<void>; @@ -611,7 +613,7 @@ export interface IDebugConfiguration { allowBreakpointsEverywhere: boolean; openDebug: 'neverOpen' | 'openOnSessionStart' | 'openOnFirstSessionStart' | 'openOnDebugBreak'; openExplorerOnEnd: boolean; - inlineValues: boolean | 'auto'; + inlineValues: boolean | 'auto' | 'on' | 'off'; // boolean for back-compat toolBarLocation: 'floating' | 'docked' | 'hidden'; showInStatusBar: 'never' | 'always' | 'onFirstSessionStart'; internalConsoleOptions: 'neverOpen' | 'openOnSessionStart' | 'openOnFirstSessionStart'; @@ -629,6 +631,7 @@ export interface IDebugConfiguration { acceptSuggestionOnEnter: 'off' | 'on'; }; focusWindowOnBreak: boolean; + focusEditorOnBreak: boolean; onTaskErrors: 'debugAnyway' | 'showErrors' | 'prompt' | 'abort'; showBreakpointsInOverviewRuler: boolean; showInlineBreakpointCandidates: boolean; @@ -636,6 +639,7 @@ export interface IDebugConfiguration { disassemblyView: { showSourceCode: boolean; }; + autoExpandLazyVariables: boolean; } export interface IGlobalConfig { @@ -770,6 +774,11 @@ export interface IDebuggerContribution extends IPlatformSpecificAdapterContribut when?: string; } +export interface IBreakpointContribution { + language: string; + when?: string; +} + export enum DebugConfigurationProviderTriggerKind { /** * `DebugConfigurationProvider.provideDebugConfigurations` is called to provide the initial debug configurations for a newly created launch.json. @@ -950,7 +959,7 @@ export interface IDebugService { /** * Sets the focused stack frame and evaluates all expressions against the newly focused stack frame, */ - focusStackFrame(focusedStackFrame: IStackFrame | undefined, thread?: IThread, session?: IDebugSession, explicit?: boolean): Promise<void>; + focusStackFrame(focusedStackFrame: IStackFrame | undefined, thread?: IThread, session?: IDebugSession, options?: { explicit?: boolean; preserveFocus?: boolean; sideBySide?: boolean; pinned?: boolean }): Promise<void>; /** * Returns true if breakpoints can be set for a given editor model. Depends on mode. @@ -1073,7 +1082,7 @@ export interface IDebugService { /** * Stops the session. If no session is specified then all sessions are stopped. */ - stopSession(session: IDebugSession | undefined, disconnect?: boolean): Promise<any>; + stopSession(session: IDebugSession | undefined, disconnect?: boolean, suspend?: boolean): Promise<any>; /** * Makes unavailable all sources with the passed uri. Source will appear as grayed out in callstack view. diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index 52994951b34..c05fc04c956 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -42,9 +42,9 @@ export class ExpressionContainer implements IExpressionContainer { constructor( protected session: IDebugSession | undefined, - protected threadId: number | undefined, + protected readonly threadId: number | undefined, private _reference: number | undefined, - private id: string, + private readonly id: string, public namedVariables: number | undefined = 0, public indexedVariables: number | undefined = 0, public memoryReference: string | undefined = undefined, @@ -152,7 +152,7 @@ export class ExpressionContainer implements IExpressionContainer { } const nameCount = new Map<string, number>(); - return response.body.variables.filter(v => !!v).map((v: IDebugProtocolVariableWithContext) => { + const vars = response.body.variables.filter(v => !!v).map((v: IDebugProtocolVariableWithContext) => { if (isString(v.value) && isString(v.name) && typeof v.variablesReference === 'number') { const count = nameCount.get(v.name) || 0; const idDuplicationIndex = count > 0 ? count.toString() : ''; @@ -161,6 +161,12 @@ export class ExpressionContainer implements IExpressionContainer { } return new Variable(this.session, this.threadId, this, 0, '', undefined, nls.localize('invalidVariableAttributes', "Invalid variable attributes"), 0, 0, undefined, { kind: 'virtual' }, undefined, undefined, false); }); + + if (this.session!.autoExpandLazyVariables) { + await Promise.all(vars.map(v => v.presentationHint?.lazy && v.evaluateLazy())); + } + + return vars; } catch (e) { return [new Variable(this.session, this.threadId, this, 0, '', undefined, e.message, 0, 0, undefined, { kind: 'virtual' }, undefined, undefined, false)]; } @@ -275,9 +281,9 @@ export class Variable extends ExpressionContainer implements IExpression { constructor( session: IDebugSession | undefined, threadId: number | undefined, - public parent: IExpressionContainer, + public readonly parent: IExpressionContainer, reference: number | undefined, - public name: string, + public readonly name: string, public evaluateName: string | undefined, value: string | undefined, namedVariables: number | undefined, @@ -285,8 +291,8 @@ export class Variable extends ExpressionContainer implements IExpression { memoryReference: string | undefined, presentationHint: DebugProtocol.VariablePresentationHint | undefined, type: string | undefined = undefined, - public variableMenuContext: string | undefined = undefined, - public available = true, + public readonly variableMenuContext: string | undefined = undefined, + public readonly available = true, startOfVariables = 0, idDuplicationIndex = '', ) { @@ -346,12 +352,12 @@ export class Scope extends ExpressionContainer implements IScope { constructor( stackFrame: IStackFrame, index: number, - public name: string, + public readonly name: string, reference: number, public expensive: boolean, namedVariables?: number, indexedVariables?: number, - public range?: IRange + public readonly range?: IRange ) { super(stackFrame.thread.session, stackFrame.thread.threadId, reference, `scope:${name}:${index}`, namedVariables, indexedVariables); } @@ -389,15 +395,15 @@ export class StackFrame implements IStackFrame { private scopes: Promise<Scope[]> | undefined; constructor( - public thread: Thread, - public frameId: number, - public source: Source, - public name: string, - public presentationHint: string | undefined, - public range: IRange, - private index: number, - public canRestart: boolean, - public instructionPointerReference?: string + public readonly thread: Thread, + public readonly frameId: number, + public readonly source: Source, + public readonly name: string, + public readonly presentationHint: string | undefined, + public readonly range: IRange, + private readonly index: number, + public readonly canRestart: boolean, + public readonly instructionPointerReference?: string ) { } getId(): string { @@ -482,7 +488,7 @@ export class Thread implements IThread { public reachedEndOfCallStack = false; public lastSteppingGranularity: DebugProtocol.SteppingGranularity | undefined; - constructor(public session: IDebugSession, public name: string, public threadId: number) { + constructor(public readonly session: IDebugSession, public name: string, public readonly threadId: number) { this.callStack = []; this.staleCallStack = []; this.stopped = false; @@ -721,7 +727,7 @@ export class MemoryRegion extends Disposable implements IMemoryRegion { export class Enablement implements IEnablement { constructor( public enabled: boolean, - private id: string + private readonly id: string ) { } getId(): string { @@ -851,7 +857,7 @@ export abstract class BaseBreakpoint extends Enablement implements IBaseBreakpoi export class Breakpoint extends BaseBreakpoint implements IBreakpoint { constructor( - private _uri: uri, + private readonly _uri: uri, private _lineNumber: number, private _column: number | undefined, enabled: boolean, @@ -1006,15 +1012,15 @@ export class FunctionBreakpoint extends BaseBreakpoint implements IFunctionBreak export class DataBreakpoint extends BaseBreakpoint implements IDataBreakpoint { constructor( - public description: string, - public dataId: string, - public canPersist: boolean, + public readonly description: string, + public readonly dataId: string, + public readonly canPersist: boolean, enabled: boolean, hitCondition: string | undefined, condition: string | undefined, logMessage: string | undefined, - public accessTypes: DebugProtocol.DataBreakpointAccessType[] | undefined, - public accessType: DebugProtocol.DataBreakpointAccessType, + public readonly accessTypes: DebugProtocol.DataBreakpointAccessType[] | undefined, + public readonly accessType: DebugProtocol.DataBreakpointAccessType, id = generateUuid() ) { super(enabled, hitCondition, condition, logMessage, id); @@ -1045,13 +1051,13 @@ export class DataBreakpoint extends BaseBreakpoint implements IDataBreakpoint { export class ExceptionBreakpoint extends BaseBreakpoint implements IExceptionBreakpoint { constructor( - public filter: string, - public label: string, + public readonly filter: string, + public readonly label: string, enabled: boolean, - public supportsCondition: boolean, + public readonly supportsCondition: boolean, condition: string | undefined, - public description: string | undefined, - public conditionDescription: string | undefined + public readonly description: string | undefined, + public readonly conditionDescription: string | undefined ) { super(enabled, undefined, condition, undefined, generateUuid()); } @@ -1079,9 +1085,9 @@ export class ExceptionBreakpoint extends BaseBreakpoint implements IExceptionBre export class InstructionBreakpoint extends BaseBreakpoint implements IInstructionBreakpoint { constructor( - public instructionReference: string, - public offset: number, - public canPersist: boolean, + public readonly instructionReference: string, + public readonly offset: number, + public readonly canPersist: boolean, enabled: boolean, hitCondition: string | undefined, condition: string | undefined, diff --git a/src/vs/workbench/contrib/debug/common/debugSchemas.ts b/src/vs/workbench/contrib/debug/common/debugSchemas.ts index 3613a1b44a4..a2a9c7f5480 100644 --- a/src/vs/workbench/contrib/debug/common/debugSchemas.ts +++ b/src/vs/workbench/contrib/debug/common/debugSchemas.ts @@ -5,7 +5,7 @@ import * as extensionsRegistry from 'vs/workbench/services/extensions/common/extensionsRegistry'; import * as nls from 'vs/nls'; -import { IDebuggerContribution, ICompound } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebuggerContribution, ICompound, IBreakpointContribution } from 'vs/workbench/contrib/debug/common/debug'; import { launchSchemaId } from 'vs/workbench/services/configuration/common/configuration'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { inputsSchema } from 'vs/workbench/services/configurationResolver/common/configurationResolverSchema'; @@ -68,7 +68,7 @@ export const debuggersExtPoint = extensionsRegistry.ExtensionsRegistry.registerE type: 'object' }, when: { - description: nls.localize('vscode.extension.contributes.debuggers.when', "Condition which must be true to enable this type of debugger. Consider using 'shellExecutionSupported', 'virtualWorkspace', 'resourceScheme' or an extension defined context key as appropriate for this."), + description: nls.localize('vscode.extension.contributes.debuggers.when', "Condition which must be true to enable this type of debugger. Consider using 'shellExecutionSupported', 'virtualWorkspace', 'resourceScheme' or an extension-defined context key as appropriate for this."), type: 'string', default: '' }, @@ -107,12 +107,8 @@ export const debuggersExtPoint = extensionsRegistry.ExtensionsRegistry.registerE } }); -export interface IRawBreakpointContribution { - language: string; -} - // breakpoints extension point #9037 -export const breakpointsExtPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint<IRawBreakpointContribution[]>({ +export const breakpointsExtPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint<IBreakpointContribution[]>({ extensionPoint: 'breakpoints', jsonSchema: { description: nls.localize('vscode.extension.contributes.breakpoints', 'Contributes breakpoints.'), @@ -127,6 +123,11 @@ export const breakpointsExtPoint = extensionsRegistry.ExtensionsRegistry.registe description: nls.localize('vscode.extension.contributes.breakpoints.language', "Allow breakpoints for this language."), type: 'string' }, + when: { + description: nls.localize('vscode.extension.contributes.breakpoints.when', "Condition which must be true to enable breakpoints in this language. Consider matching this to the debugger when clause as appropriate."), + type: 'string', + default: '' + } } } } diff --git a/src/vs/workbench/contrib/debug/common/debugViewModel.ts b/src/vs/workbench/contrib/debug/common/debugViewModel.ts index a6444a1f5fa..46468bb81d7 100644 --- a/src/vs/workbench/contrib/debug/common/debugViewModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugViewModel.ts @@ -5,7 +5,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { CONTEXT_DISASSEMBLE_REQUEST_SUPPORTED, CONTEXT_EXPRESSION_SELECTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_FOCUSED_STACK_FRAME_HAS_INSTRUCTION_POINTER_REFERENCE, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_MULTI_SESSION_DEBUG, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_SET_EXPRESSION_SUPPORTED, CONTEXT_SET_VARIABLE_SUPPORTED, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED, IDebugSession, IExpression, IExpressionContainer, IStackFrame, IThread, IViewModel } from 'vs/workbench/contrib/debug/common/debug'; +import { CONTEXT_DISASSEMBLE_REQUEST_SUPPORTED, CONTEXT_EXPRESSION_SELECTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_FOCUSED_STACK_FRAME_HAS_INSTRUCTION_POINTER_REFERENCE, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_MULTI_SESSION_DEBUG, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_SET_EXPRESSION_SUPPORTED, CONTEXT_SET_VARIABLE_SUPPORTED, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, CONTEXT_SUSPEND_DEBUGGEE_SUPPORTED, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED, IDebugSession, IExpression, IExpressionContainer, IStackFrame, IThread, IViewModel } from 'vs/workbench/contrib/debug/common/debug'; import { isSessionAttach } from 'vs/workbench/contrib/debug/common/debugUtils'; export class ViewModel implements IViewModel { @@ -31,7 +31,8 @@ export class ViewModel implements IViewModel { private setVariableSupported!: IContextKey<boolean>; private setExpressionSupported!: IContextKey<boolean>; private multiSessionDebug!: IContextKey<boolean>; - private terminateDebuggeeSuported!: IContextKey<boolean>; + private terminateDebuggeeSupported!: IContextKey<boolean>; + private suspendDebuggeeSupported!: IContextKey<boolean>; private disassembleRequestSupported!: IContextKey<boolean>; private focusedStackFrameHasInstructionPointerReference!: IContextKey<Boolean>; @@ -47,7 +48,8 @@ export class ViewModel implements IViewModel { this.setVariableSupported = CONTEXT_SET_VARIABLE_SUPPORTED.bindTo(contextKeyService); this.setExpressionSupported = CONTEXT_SET_EXPRESSION_SUPPORTED.bindTo(contextKeyService); this.multiSessionDebug = CONTEXT_MULTI_SESSION_DEBUG.bindTo(contextKeyService); - this.terminateDebuggeeSuported = CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED.bindTo(contextKeyService); + this.terminateDebuggeeSupported = CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED.bindTo(contextKeyService); + this.suspendDebuggeeSupported = CONTEXT_SUSPEND_DEBUGGEE_SUPPORTED.bindTo(contextKeyService); this.disassembleRequestSupported = CONTEXT_DISASSEMBLE_REQUEST_SUPPORTED.bindTo(contextKeyService); this.focusedStackFrameHasInstructionPointerReference = CONTEXT_FOCUSED_STACK_FRAME_HAS_INSTRUCTION_POINTER_REFERENCE.bindTo(contextKeyService); }); @@ -85,7 +87,8 @@ export class ViewModel implements IViewModel { this.jumpToCursorSupported.set(session ? !!session.capabilities.supportsGotoTargetsRequest : false); this.setVariableSupported.set(session ? !!session.capabilities.supportsSetVariable : false); this.setExpressionSupported.set(session ? !!session.capabilities.supportsSetExpression : false); - this.terminateDebuggeeSuported.set(session ? !!session.capabilities.supportTerminateDebuggee : false); + this.terminateDebuggeeSupported.set(session ? !!session.capabilities.supportTerminateDebuggee : false); + this.suspendDebuggeeSupported.set(session ? !!session.capabilities.supportSuspendDebuggee : false); this.disassembleRequestSupported.set(!!session?.capabilities.supportsDisassembleRequest); this.focusedStackFrameHasInstructionPointerReference.set(!!stackFrame?.instructionPointerReference); const attach = !!session && isSessionAttach(session); diff --git a/src/vs/workbench/contrib/debug/common/debugger.ts b/src/vs/workbench/contrib/debug/common/debugger.ts index c1c0166702d..8a41ba6c603 100644 --- a/src/vs/workbench/contrib/debug/common/debugger.ts +++ b/src/vs/workbench/contrib/debug/common/debugger.ts @@ -217,6 +217,7 @@ export class Debugger implements IDebugger { const attributes: IJSONSchema = this.debuggerContribution.configurationAttributes[request]; const defaultRequired = ['name', 'type', 'request']; attributes.required = attributes.required && attributes.required.length ? defaultRequired.concat(attributes.required) : defaultRequired; + attributes.additionalProperties = false; attributes.type = 'object'; if (!attributes.properties) { attributes.properties = {}; @@ -239,38 +240,37 @@ export class Debugger implements IDebugger { $ref: `#/definitions/common/properties/${prop}` }; } - definitions[definitionId] = attributes; - Object.keys(properties).forEach(name => { // Use schema allOf property to get independent error reporting #21113 ConfigurationResolverUtils.applyDeprecatedVariableMessage(properties[name]); }); - const result = { - allOf: [{ - $ref: `#/definitions/${definitionId}` - }, { - properties: { - windows: { - $ref: `#/definitions/${definitionId}`, - description: nls.localize('debugWindowsConfiguration', "Windows specific launch configuration attributes."), - required: [], - }, - osx: { - $ref: `#/definitions/${definitionId}`, - description: nls.localize('debugOSXConfiguration', "OS X specific launch configuration attributes."), - required: [], - }, - linux: { - $ref: `#/definitions/${definitionId}`, - description: nls.localize('debugLinuxConfiguration', "Linux specific launch configuration attributes."), - required: [], - } + definitions[definitionId] = { ...attributes }; + + // Don't add the OS props to the real attributes object so they don't show up in 'definitions' + const attributesCopy = { ...attributes }; + attributesCopy.properties = { + ...properties, + ...{ + windows: { + $ref: `#/definitions/${definitionId}`, + description: nls.localize('debugWindowsConfiguration', "Windows specific launch configuration attributes."), + required: [], + }, + osx: { + $ref: `#/definitions/${definitionId}`, + description: nls.localize('debugOSXConfiguration', "OS X specific launch configuration attributes."), + required: [], + }, + linux: { + $ref: `#/definitions/${definitionId}`, + description: nls.localize('debugLinuxConfiguration', "Linux specific launch configuration attributes."), + required: [], } - }] + } }; - return result; + return attributesCopy; }); } } diff --git a/src/vs/workbench/contrib/debug/node/debugAdapter.ts b/src/vs/workbench/contrib/debug/node/debugAdapter.ts index 6347d7ba2a5..4f0bf897787 100644 --- a/src/vs/workbench/contrib/debug/node/debugAdapter.ts +++ b/src/vs/workbench/contrib/debug/node/debugAdapter.ts @@ -13,7 +13,7 @@ import * as strings from 'vs/base/common/strings'; import * as objects from 'vs/base/common/objects'; import * as platform from 'vs/base/common/platform'; import { ExtensionsChannelId } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { IOutputService } from 'vs/workbench/contrib/output/common/output'; +import { IOutputService } from 'vs/workbench/services/output/common/output'; import { IDebugAdapterExecutable, IDebuggerContribution, IPlatformSpecificAdapterContribution, IDebugAdapterServer, IDebugAdapterNamedPipeServer } from 'vs/workbench/contrib/debug/common/debug'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { AbstractDebugAdapter } from '../common/abstractDebugAdapter'; diff --git a/src/vs/workbench/contrib/debug/node/terminals.ts b/src/vs/workbench/contrib/debug/node/terminals.ts index c27c88f27a6..5418d9e8ffc 100644 --- a/src/vs/workbench/contrib/debug/node/terminals.ts +++ b/src/vs/workbench/contrib/debug/node/terminals.ts @@ -121,7 +121,8 @@ export function prepareCommand(shell: string, args: string[], cwd?: string, env? quote = (s: string) => { s = s.replace(/\"/g, '""'); - return (s.indexOf(' ') >= 0 || s.indexOf('"') >= 0 || s.length === 0) ? `"${s}"` : s; + s = s.replace(/([><!^&|])/g, '^$1'); + return (' "'.split('').some(char => s.includes(char)) || s.length === 0) ? `"${s}"` : s; }; if (cwd) { @@ -138,7 +139,7 @@ export function prepareCommand(shell: string, args: string[], cwd?: string, env? if (value === null) { command += `set "${key}=" && `; } else { - value = value.replace(/[\^\&\|\<\>]/g, s => `^${s}`); + value = value.replace(/[&^|<>]/g, s => `^${s}`); command += `set "${key}=${value}" && `; } } @@ -154,8 +155,8 @@ export function prepareCommand(shell: string, args: string[], cwd?: string, env? case ShellType.bash: { quote = (s: string) => { - s = s.replace(/(["'\\\$])/g, '\\$1'); - return (s.indexOf(' ') >= 0 || s.indexOf(';') >= 0 || s.length === 0) ? `"${s}"` : s; + s = s.replace(/(["'\\\$!><#()\[\]*&^|])/g, '\\$1'); + return (' ;'.split('').some(char => s.includes(char)) || s.length === 0) ? `"${s}"` : s; }; const hardQuote = (s: string) => { diff --git a/src/vs/workbench/contrib/debug/test/browser/mockDebug.ts b/src/vs/workbench/contrib/debug/test/browser/mockDebug.ts index 81c394466ec..1bebaa31adb 100644 --- a/src/vs/workbench/contrib/debug/test/browser/mockDebug.ts +++ b/src/vs/workbench/contrib/debug/test/browser/mockDebug.ts @@ -168,6 +168,8 @@ export class MockDebugService implements IDebugService { } export class MockSession implements IDebugSession { + readonly autoExpandLazyVariables = false; + getMemory(memoryReference: string): IMemoryRegion { throw new Error('Method not implemented.'); } diff --git a/src/vs/workbench/contrib/dropIntoEditor/browser/dropIntoEditor.contibution.ts b/src/vs/workbench/contrib/dropIntoEditor/browser/dropIntoEditor.contibution.ts new file mode 100644 index 00000000000..7e1380f1d1f --- /dev/null +++ b/src/vs/workbench/contrib/dropIntoEditor/browser/dropIntoEditor.contibution.ts @@ -0,0 +1,151 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { distinct } from 'vs/base/common/arrays'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Mimes } from 'vs/base/common/mime'; +import { relativePath } from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; +import { IPosition } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; +import { IEditorContribution } from 'vs/editor/common/editorCommon'; +import { IDataTransferItem } from 'vs/editor/common/languages'; +import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import { performSnippetEdit } from 'vs/editor/contrib/snippet/browser/snippetController2'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { extractEditorsDropData } from 'vs/workbench/browser/dnd'; +import { IDataTransfer } from 'vs/workbench/common/dnd'; + + +export class DropIntoEditorController extends Disposable implements IEditorContribution { + + public static readonly ID = 'editor.contrib.dropIntoEditorController'; + + constructor( + editor: ICodeEditor, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, + @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, + ) { + super(); + + editor.onDropIntoEditor(e => this.onDropIntoEditor(editor, e.position, e.event)); + } + + private async onDropIntoEditor(editor: ICodeEditor, position: IPosition, dragEvent: DragEvent) { + if (!dragEvent.dataTransfer || !editor.hasModel()) { + return; + } + + const model = editor.getModel(); + const modelVersionNow = model.getVersionId(); + + const textEditorDataTransfer: IDataTransfer = new Map<string, IDataTransferItem>(); + for (const item of dragEvent.dataTransfer.items) { + if (item.kind === 'string') { + const type = item.type; + const asStringValue = new Promise<string>(resolve => item.getAsString(resolve)); + textEditorDataTransfer.set(type, { + asString: () => asStringValue, + value: undefined + }); + } + } + + if (!textEditorDataTransfer.has(Mimes.uriList.toLowerCase())) { + const editorData = (await this._instantiationService.invokeFunction(extractEditorsDropData, dragEvent)) + .filter(input => input.resource) + .map(input => input.resource!.toString()); + + if (editorData.length) { + const str = distinct(editorData).join('\n'); + textEditorDataTransfer.set(Mimes.uriList.toLowerCase(), { + asString: () => Promise.resolve(str), + value: undefined + }); + } + } + + if (textEditorDataTransfer.size === 0) { + return; + } + + if (editor.getModel().getVersionId() !== modelVersionNow) { + return; + } + + const cts = new CancellationTokenSource(); + editor.onDidDispose(() => cts.cancel()); + model.onDidChangeContent(() => cts.cancel()); + + const ordered = this._languageFeaturesService.documentOnDropEditProvider.ordered(model); + for (const provider of ordered) { + const edit = await provider.provideDocumentOnDropEdits(model, position, textEditorDataTransfer, cts.token); + if (cts.token.isCancellationRequested || editor.getModel().getVersionId() !== modelVersionNow) { + return; + } + + if (edit) { + performSnippetEdit(editor, edit); + return; + } + } + + return this.doDefaultDrop(editor, position, textEditorDataTransfer, cts.token); + } + + private async doDefaultDrop(editor: ICodeEditor, position: IPosition, textEditorDataTransfer: IDataTransfer, token: CancellationToken): Promise<void> { + const range = new Range(position.lineNumber, position.column, position.lineNumber, position.column); + + const urlListEntry = textEditorDataTransfer.get('text/uri-list'); + if (urlListEntry) { + const urlList = await urlListEntry.asString(); + return this.doUriListDrop(editor, range, urlList, token); + } + + const textEntry = textEditorDataTransfer.get('text') ?? textEditorDataTransfer.get(Mimes.text); + if (textEntry) { + const text = await textEntry.asString(); + performSnippetEdit(editor, { range, snippet: text }); + } + } + + private async doUriListDrop(editor: ICodeEditor, range: Range, urlList: string, token: CancellationToken): Promise<void> { + const uris: URI[] = []; + for (const resource of urlList.split('\n')) { + try { + uris.push(URI.parse(resource)); + } catch { + // noop + } + } + + if (!uris.length) { + return; + } + + const snippet = uris + .map(uri => { + const root = this._workspaceContextService.getWorkspaceFolder(uri); + if (root) { + const rel = relativePath(root.uri, uri); + if (rel) { + return rel; + } + } + return uri.fsPath; + }) + .join(' '); + + performSnippetEdit(editor, { range, snippet }); + } +} + + +registerEditorContribution(DropIntoEditorController.ID, DropIntoEditorController); diff --git a/src/vs/workbench/contrib/emmet/browser/emmetActions.ts b/src/vs/workbench/contrib/emmet/browser/emmetActions.ts index 21292c80a47..08b8e4d9c32 100644 --- a/src/vs/workbench/contrib/emmet/browser/emmetActions.ts +++ b/src/vs/workbench/contrib/emmet/browser/emmetActions.ts @@ -93,7 +93,7 @@ export abstract class EmmetEditorAction extends EditorAction { } const position = selection.getStartPosition(); - model.tokenizeIfCheap(position.lineNumber); + model.tokenization.tokenizeIfCheap(position.lineNumber); const languageId = model.getLanguageIdAtPosition(position.lineNumber, position.column); const syntax = languageId.split('.').pop(); diff --git a/src/vs/workbench/contrib/experiments/common/experimentService.ts b/src/vs/workbench/contrib/experiments/common/experimentService.ts index a0d52ac4b52..0770dbb143e 100644 --- a/src/vs/workbench/contrib/experiments/common/experimentService.ts +++ b/src/vs/workbench/contrib/experiments/common/experimentService.ts @@ -21,6 +21,7 @@ import { asJson, IRequestService } from 'vs/platform/request/common/request'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { ITelemetryService, lastSessionDateStorageKey } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceTagsService } from 'vs/workbench/contrib/tags/common/workspaceTags'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { ITextFileEditorModel, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; @@ -182,7 +183,8 @@ export class ExperimentService extends Disposable implements IExperimentService @IConfigurationService private readonly configurationService: IConfigurationService, @IProductService private readonly productService: IProductService, @IWorkspaceTagsService private readonly workspaceTagsService: IWorkspaceTagsService, - @IExtensionService private readonly extensionService: IExtensionService + @IExtensionService private readonly extensionService: IExtensionService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService ) { super(); @@ -227,6 +229,10 @@ export class ExperimentService extends Disposable implements IExperimentService } protected async getExperiments(): Promise<IRawExperiment[] | null> { + if (this.environmentService.enableSmokeTestDriver || this.environmentService.extensionTestsLocationURI) { + return []; // TODO@sbatten add CLI argument (https://github.com/microsoft/vscode-internalbacklog/issues/2855) + } + const experimentsUrl = this.configurationService.getValue<string>('_workbench.experimentsUrl') || this.productService.experimentsUrl; if (!experimentsUrl || this.configurationService.getValue('workbench.enableExperiments') === false) { return []; diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index cb560efaaa3..8e2d1a5badd 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -71,6 +71,7 @@ import { MarkdownString } from 'vs/base/common/htmlContent'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; import { ViewContainerLocation } from 'vs/workbench/common/views'; import { IExtensionGalleryService, IGalleryExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; +import semver = require('vs/base/common/semver/semver'); class NavBar extends Disposable { @@ -183,10 +184,10 @@ class VersionWidget extends ExtensionWithDifferentGalleryVersionWidget { this.render(); } render(): void { - if (!this.extension) { + if (!this.extension || !semver.valid(this.extension.version)) { return; } - this.element.textContent = `v${this.gallery ? this.gallery.version : this.extension.version}`; + this.element.textContent = `v${this.gallery?.version ?? this.extension.version}`; } } diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 41515d1c2e2..ebbd930216f 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -200,9 +200,12 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration) } }, additionalProperties: false, - default: { - 'pub.name': false - } + default: {}, + defaultSnippets: [{ + 'body': { + 'pub.name': false + } + }] }, 'extensions.experimental.affinity': { type: 'object', @@ -214,9 +217,12 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration) } }, additionalProperties: false, - default: { - 'pub.name': 1 - } + default: {}, + defaultSnippets: [{ + 'body': { + 'pub.name': 1 + } + }] }, [WORKSPACE_TRUST_EXTENSION_SUPPORT]: { type: 'object', diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 2de5c8e1411..dd69b73511d 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -601,7 +601,7 @@ export abstract class InstallInOtherServerAction extends ExtensionAction { this.extensionsWorkbenchService.open(this.extension); alert(localize('installExtensionStart', "Installing extension {0} started. An editor is now open with more details on this extension", this.extension.displayName)); if (this.extension.gallery) { - await this.server.extensionManagementService.installFromGallery(this.extension.gallery); + await this.server.extensionManagementService.installFromGallery(this.extension.gallery, { installPreReleaseVersion: this.extension.local?.preRelease }); } else { const vsix = await this.extension.server!.extensionManagementService.zip(this.extension.local!); await this.server.extensionManagementService.install(vsix); @@ -2348,6 +2348,11 @@ export class ExtensionStatusAction extends ExtensionAction { } } + if (isEnabled && !isRunning && !this.extension.local.isValid) { + const errors = this.extension.local.validations.filter(([severity]) => severity === Severity.Error).map(([, message]) => message); + this.updateStatus({ icon: errorIcon, message: new MarkdownString(errors.join(' ').trim()) }, true); + } + } private updateStatus(status: ExtensionStatus | undefined, updateClass: boolean): void { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts index f51545869aa..cf3f3473212 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts @@ -719,11 +719,9 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE const message = err && err.message || ''; if (/ECONNREFUSED/.test(message)) { - const error = createErrorWithActions(localize('suggestProxyError', "Marketplace returned 'ECONNREFUSED'. Please check the 'http.proxy' setting."), { - actions: [ - new Action('open user settings', localize('open user settings', "Open User Settings"), undefined, true, () => this.preferencesService.openUserSettings()) - ] - }); + const error = createErrorWithActions(localize('suggestProxyError', "Marketplace returned 'ECONNREFUSED'. Please check the 'http.proxy' setting."), [ + new Action('open user settings', localize('open user settings', "Open User Settings"), undefined, true, () => this.preferencesService.openUserSettings()) + ]); this.notificationService.error(error); return; diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index d7455895d9e..de628c54a8d 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -972,11 +972,9 @@ export class ExtensionsListView extends ViewPane { const message = err && err.message || ''; if (/ECONNREFUSED/.test(message)) { - const error = createErrorWithActions(localize('suggestProxyError', "Marketplace returned 'ECONNREFUSED'. Please check the 'http.proxy' setting."), { - actions: [ - new Action('open user settings', localize('open user settings', "Open User Settings"), undefined, true, () => this.preferencesService.openUserSettings()) - ] - }); + const error = createErrorWithActions(localize('suggestProxyError', "Marketplace returned 'ECONNREFUSED'. Please check the 'http.proxy' setting."), [ + new Action('open user settings', localize('open user settings', "Open User Settings"), undefined, true, () => this.preferencesService.openUserSettings()) + ]); this.notificationService.error(error); return; diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts index 522cc6346be..76d062f4c5a 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/extensionsWidgets'; +import * as semver from 'vs/base/common/semver/semver'; import { Disposable, toDisposable, DisposableStore, MutableDisposable, IDisposable } from 'vs/base/common/lifecycle'; import { IExtension, IExtensionsWorkbenchService, IExtensionContainer, ExtensionState, ExtensionEditorTab } from 'vs/workbench/contrib/extensions/common/extensions'; import { append, $ } from 'vs/base/browser/dom'; @@ -480,7 +481,10 @@ export class ExtensionHoverWidget extends ExtensionWidget { } const markdown = new MarkdownString('', { isTrusted: true, supportThemeIcons: true }); - markdown.appendMarkdown(`**${this.extension.displayName}** <span style="background-color:#8080802B;">** _v${this.extension.version}_** </span>`); + markdown.appendMarkdown(`**${this.extension.displayName}**`); + if (semver.valid(this.extension.version)) { + markdown.appendMarkdown(` <span style="background-color:#8080802B;">** _v${this.extension.version}_** </span>`); + } if (this.extension.state === ExtensionState.Installed ? this.extension.local?.isPreReleaseVersion : this.extension.gallery?.properties.isPreReleaseVersion) { const extensionPreReleaseIcon = this.themeService.getColorTheme().getColor(extensionPreReleaseIconColor); markdown.appendMarkdown(`** ** <span style="color:#ffffff;background-color:${extensionPreReleaseIcon ? Color.Format.CSS.formatHex(extensionPreReleaseIcon) : '#ffffff'};"> $(${preReleaseIcon.id}) ${localize('pre-release-label', "Pre-Release")} </span>`); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 2b0a0fad05b..57acb10a415 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -8,7 +8,7 @@ import * as semver from 'vs/base/common/semver/semver'; import { Event, Emitter } from 'vs/base/common/event'; import { index, distinct } from 'vs/base/common/arrays'; import { Promises, ThrottledDelayer } from 'vs/base/common/async'; -import { canceled, isCancellationError } from 'vs/base/common/errors'; +import { CancellationError, isCancellationError } from 'vs/base/common/errors'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { IPager, singlePagePager } from 'vs/base/common/paging'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -203,17 +203,25 @@ export class Extension implements IExtension { } get outdated(): boolean { - if (!this.gallery || !this.local) { - return false; - } - if (!this.local.preRelease && this.gallery.properties.isPreReleaseVersion) { - return false; - } - if (semver.gt(this.latestVersion, this.version)) { - return true; - } - if (this.outdatedTargetPlatform) { - return true; + try { + if (!this.gallery || !this.local) { + return false; + } + // Do not allow updating system extensions in stable + if (this.type === ExtensionType.System && this.productService.quality === 'stable') { + return false; + } + if (!this.local.preRelease && this.gallery.properties.isPreReleaseVersion) { + return false; + } + if (semver.gt(this.latestVersion, this.version)) { + return true; + } + if (this.outdatedTargetPlatform) { + return true; + } + } catch (error) { + /* Ignore */ } return false; } @@ -1057,8 +1065,8 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension // Skip if check updates only for builtin extensions and current extension is not builtin. continue; } - if (installed.isBuiltin && !installed.local?.identifier.uuid) { - // Skip if the builtin extension does not have Marketplace id + if (installed.isBuiltin && (!installed.local?.identifier.uuid || this.productService.quality !== 'stable')) { + // Skip if the builtin extension does not have Marketplace identifier or the current quality is not stable. continue; } infos.push({ ...installed.identifier, preRelease: !!installed.local?.preRelease }); @@ -1353,7 +1361,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } } ], { - onCancel: () => reject(canceled()) + onCancel: () => reject(new CancellationError()) }); }); } diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extension.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extension.test.ts index 335679af2a9..f07341fe991 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extension.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extension.test.ts @@ -12,6 +12,7 @@ import { URI } from 'vs/base/common/uri'; import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { generateUuid } from 'vs/base/common/uuid'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { IProductService } from 'vs/platform/product/common/productService'; suite('Extension Test', () => { @@ -19,6 +20,7 @@ suite('Extension Test', () => { setup(() => { instantiationService = new TestInstantiationService(); + instantiationService.stub(IProductService, <Partial<IProductService>>{ quality: 'insiders' }); }); test('extension is not outdated when there is no local and gallery', () => { @@ -51,6 +53,12 @@ suite('Extension Test', () => { assert.strictEqual(extension.outdated, true); }); + test('extension is not outdated when local is built in and older than gallery but product quality is stable', () => { + instantiationService.stub(IProductService, <Partial<IProductService>>{ quality: 'stable' }); + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, undefined, aLocalExtension('somext', { version: '1.0.0' }, { type: ExtensionType.System }), aGalleryExtension('somext', { version: '1.0.1' })); + assert.strictEqual(extension.outdated, false); + }); + test('extension is outdated when local and gallery are on same version but on different target platforms', () => { const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, undefined, aLocalExtension('somext', {}, { targetPlatform: TargetPlatform.WIN32_IA32 }), aGalleryExtension('somext', {}, { targetPlatform: TargetPlatform.WIN32_X64 })); assert.strictEqual(extension.outdated, true); diff --git a/src/vs/workbench/contrib/feedback/browser/feedback.ts b/src/vs/workbench/contrib/feedback/browser/feedback.ts index 46fc36b304e..77be54e32c4 100644 --- a/src/vs/workbench/contrib/feedback/browser/feedback.ts +++ b/src/vs/workbench/contrib/feedback/browser/feedback.ts @@ -4,15 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/feedback'; -import * as nls from 'vs/nls'; +import { localize } from 'vs/nls'; import { IDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; -import * as dom from 'vs/base/browser/dom'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IIntegrityService } from 'vs/workbench/services/integrity/common/integrity'; import { IThemeService, registerThemingParticipant, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; import { attachButtonStyler, attachStylerCallback } from 'vs/platform/theme/common/styler'; import { editorWidgetBackground, editorWidgetForeground, widgetShadow, inputBorder, inputForeground, inputBackground, inputActiveOptionBorder, editorBackground, textLinkForeground, contrastBorder } from 'vs/platform/theme/common/colorRegistry'; +import { append, $, addDisposableListener, EventType, EventHelper, prepend } from 'vs/base/browser/dom'; import { IAnchor } from 'vs/base/browser/ui/contextview/contextview'; import { Button } from 'vs/base/browser/ui/button/button'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -114,25 +114,26 @@ export class FeedbackWidget extends Disposable { container.classList.add('monaco-menu-container'); // Form - this.feedbackForm = dom.append<HTMLFormElement>(container, dom.$('form.feedback-form')); + this.feedbackForm = append<HTMLFormElement>(container, $('form.feedback-form')); this.feedbackForm.setAttribute('action', 'javascript:void(0);'); // Title - dom.append(this.feedbackForm, dom.$('h2.title')).textContent = nls.localize("label.sendASmile", "Tweet us your feedback."); + append(this.feedbackForm, $('h2.title')).textContent = localize("label.sendASmile", "Tweet us your feedback."); // Close Button (top right) - const closeBtn = dom.append(this.feedbackForm, dom.$('div.cancel' + Codicon.close.cssSelector)); + const closeBtn = append(this.feedbackForm, $(`div.cancel${Codicon.close.cssSelector}`)); closeBtn.tabIndex = 0; closeBtn.setAttribute('role', 'button'); - closeBtn.title = nls.localize('close', "Close"); + closeBtn.title = localize('close', "Close"); - disposables.add(dom.addDisposableListener(container, dom.EventType.KEY_DOWN, keyboardEvent => { + disposables.add(addDisposableListener(container, EventType.KEY_DOWN, keyboardEvent => { const standardKeyboardEvent = new StandardKeyboardEvent(keyboardEvent); if (standardKeyboardEvent.keyCode === KeyCode.Escape) { this.hide(); } })); - disposables.add(dom.addDisposableListener(closeBtn, dom.EventType.MOUSE_OVER, () => { + + disposables.add(addDisposableListener(closeBtn, EventType.MOUSE_OVER, () => { const theme = this.themeService.getColorTheme(); let darkenFactor: number | undefined; switch (theme.type) { @@ -155,47 +156,47 @@ export class FeedbackWidget extends Disposable { } })); - disposables.add(dom.addDisposableListener(closeBtn, dom.EventType.MOUSE_OUT, () => { + disposables.add(addDisposableListener(closeBtn, EventType.MOUSE_OUT, () => { closeBtn.style.backgroundColor = ''; })); this.invoke(closeBtn, disposables, () => this.hide()); // Content - const content = dom.append(this.feedbackForm, dom.$('div.content')); + const content = append(this.feedbackForm, $('div.content')); // Sentiment Buttons - const sentimentContainer = dom.append(content, dom.$('div')); + const sentimentContainer = append(content, $('div')); if (!this.isPure) { - dom.append(sentimentContainer, dom.$('span')).textContent = nls.localize("patchedVersion1", "Your installation is corrupt."); + append(sentimentContainer, $('span')).textContent = localize("patchedVersion1", "Your installation is corrupt."); sentimentContainer.appendChild(document.createElement('br')); - dom.append(sentimentContainer, dom.$('span')).textContent = nls.localize("patchedVersion2", "Please specify this if you submit a bug."); + append(sentimentContainer, $('span')).textContent = localize("patchedVersion2", "Please specify this if you submit a bug."); sentimentContainer.appendChild(document.createElement('br')); } - dom.append(sentimentContainer, dom.$('span')).textContent = nls.localize("sentiment", "How was your experience?"); + append(sentimentContainer, $('span')).textContent = localize("sentiment", "How was your experience?"); - const feedbackSentiment = dom.append(sentimentContainer, dom.$('div.feedback-sentiment')); + const feedbackSentiment = append(sentimentContainer, $('div.feedback-sentiment')); // Sentiment: Smiley - this.smileyInput = dom.append(feedbackSentiment, dom.$('div.sentiment')); + this.smileyInput = append(feedbackSentiment, $('div.sentiment')); this.smileyInput.classList.add('smile'); this.smileyInput.setAttribute('aria-checked', 'false'); - this.smileyInput.setAttribute('aria-label', nls.localize('smileCaption', "Happy Feedback Sentiment")); + this.smileyInput.setAttribute('aria-label', localize('smileCaption', "Happy Feedback Sentiment")); this.smileyInput.setAttribute('role', 'checkbox'); - this.smileyInput.title = nls.localize('smileCaption', "Happy Feedback Sentiment"); + this.smileyInput.title = localize('smileCaption', "Happy Feedback Sentiment"); this.smileyInput.tabIndex = 0; this.invoke(this.smileyInput, disposables, () => this.setSentiment(true)); // Sentiment: Frowny - this.frownyInput = dom.append(feedbackSentiment, dom.$('div.sentiment')); + this.frownyInput = append(feedbackSentiment, $('div.sentiment')); this.frownyInput.classList.add('frown'); this.frownyInput.setAttribute('aria-checked', 'false'); - this.frownyInput.setAttribute('aria-label', nls.localize('frownCaption', "Sad Feedback Sentiment")); + this.frownyInput.setAttribute('aria-label', localize('frownCaption', "Sad Feedback Sentiment")); this.frownyInput.setAttribute('role', 'checkbox'); - this.frownyInput.title = nls.localize('frownCaption', "Sad Feedback Sentiment"); + this.frownyInput.title = localize('frownCaption', "Sad Feedback Sentiment"); this.frownyInput.tabIndex = 0; this.invoke(this.frownyInput, disposables, () => this.setSentiment(false)); @@ -209,23 +210,23 @@ export class FeedbackWidget extends Disposable { } // Contact Us Box - const contactUsContainer = dom.append(content, dom.$('div.contactus')); + const contactUsContainer = append(content, $('div.contactus')); - dom.append(contactUsContainer, dom.$('span')).textContent = nls.localize("other ways to contact us", "Other ways to contact us"); + append(contactUsContainer, $('span')).textContent = localize("other ways to contact us", "Other ways to contact us"); - const channelsContainer = dom.append(contactUsContainer, dom.$('div.channels')); + const channelsContainer = append(contactUsContainer, $('div.channels')); // Contact: Submit a Bug - const submitBugLinkContainer = dom.append(channelsContainer, dom.$('div')); + const submitBugLinkContainer = append(channelsContainer, $('div')); - const submitBugLink = dom.append(submitBugLinkContainer, dom.$('a')); + const submitBugLink = append(submitBugLinkContainer, $('a')); submitBugLink.setAttribute('target', '_blank'); submitBugLink.setAttribute('href', '#'); - submitBugLink.textContent = nls.localize("submit a bug", "Submit a bug"); + submitBugLink.textContent = localize("submit a bug", "Submit a bug"); submitBugLink.tabIndex = 0; - disposables.add(dom.addDisposableListener(submitBugLink, 'click', e => { - dom.EventHelper.stop(e); + disposables.add(addDisposableListener(submitBugLink, 'click', e => { + EventHelper.stop(e); const actionId = 'workbench.action.openIssueReporter'; this.commandService.executeCommand(actionId); this.hide(); @@ -234,57 +235,57 @@ export class FeedbackWidget extends Disposable { // Contact: Request a Feature if (!!this.requestFeatureLink) { - const requestFeatureLinkContainer = dom.append(channelsContainer, dom.$('div')); + const requestFeatureLinkContainer = append(channelsContainer, $('div')); - const requestFeatureLink = dom.append(requestFeatureLinkContainer, dom.$('a')); + const requestFeatureLink = append(requestFeatureLinkContainer, $('a')); requestFeatureLink.setAttribute('target', '_blank'); requestFeatureLink.setAttribute('href', this.requestFeatureLink); - requestFeatureLink.textContent = nls.localize("request a missing feature", "Request a missing feature"); + requestFeatureLink.textContent = localize("request a missing feature", "Request a missing feature"); requestFeatureLink.tabIndex = 0; - disposables.add(dom.addDisposableListener(requestFeatureLink, 'click', e => this.hide())); + disposables.add(addDisposableListener(requestFeatureLink, 'click', e => this.hide())); } // Remaining Characters - const remainingCharacterCountContainer = dom.append(this.feedbackForm, dom.$('h3')); - remainingCharacterCountContainer.textContent = nls.localize("tell us why", "Tell us why?"); + const remainingCharacterCountContainer = append(this.feedbackForm, $('h3')); + remainingCharacterCountContainer.textContent = localize("tell us why", "Tell us why?"); - this.remainingCharacterCount = dom.append(remainingCharacterCountContainer, dom.$('span.char-counter')); + this.remainingCharacterCount = append(remainingCharacterCountContainer, $('span.char-counter')); this.remainingCharacterCount.textContent = this.getCharCountText(0); // Feedback Input Form - this.feedbackDescriptionInput = dom.append<HTMLTextAreaElement>(this.feedbackForm, dom.$('textarea.feedback-description')); + this.feedbackDescriptionInput = append<HTMLTextAreaElement>(this.feedbackForm, $('textarea.feedback-description')); this.feedbackDescriptionInput.rows = 3; this.feedbackDescriptionInput.maxLength = this.maxFeedbackCharacters; this.feedbackDescriptionInput.textContent = this.feedback; this.feedbackDescriptionInput.required = true; - this.feedbackDescriptionInput.setAttribute('aria-label', nls.localize("feedbackTextInput", "Tell us your feedback")); + this.feedbackDescriptionInput.setAttribute('aria-label', localize("feedbackTextInput", "Tell us your feedback")); this.feedbackDescriptionInput.focus(); - disposables.add(dom.addDisposableListener(this.feedbackDescriptionInput, 'keyup', () => this.updateCharCountText())); + disposables.add(addDisposableListener(this.feedbackDescriptionInput, 'keyup', () => this.updateCharCountText())); // Feedback Input Form Buttons Container - const buttonsContainer = dom.append(this.feedbackForm, dom.$('div.form-buttons')); + const buttonsContainer = append(this.feedbackForm, $('div.form-buttons')); // Checkbox: Hide Feedback Smiley - const hideButtonContainer = dom.append(buttonsContainer, dom.$('div.hide-button-container')); + const hideButtonContainer = append(buttonsContainer, $('div.hide-button-container')); - this.hideButton = dom.append(hideButtonContainer, dom.$('input.hide-button')) as HTMLInputElement; + this.hideButton = append(hideButtonContainer, $('input.hide-button')) as HTMLInputElement; this.hideButton.type = 'checkbox'; this.hideButton.checked = true; this.hideButton.id = 'hide-button'; - const hideButtonLabel = dom.append(hideButtonContainer, dom.$('label')); + const hideButtonLabel = append(hideButtonContainer, $('label')); hideButtonLabel.setAttribute('for', 'hide-button'); - hideButtonLabel.textContent = nls.localize('showFeedback', "Show Feedback Icon in Status Bar"); + hideButtonLabel.textContent = localize('showFeedback', "Show Feedback Icon in Status Bar"); // Button: Send Feedback this.sendButton = new Button(buttonsContainer); this.sendButton.enabled = false; - this.sendButton.label = nls.localize('tweet', "Tweet"); - dom.prepend(this.sendButton.element, dom.$('span' + Codicon.twitter.cssSelector)); + this.sendButton.label = localize('tweet', "Tweet"); + prepend(this.sendButton.element, $(`span${Codicon.twitter.cssSelector}`)); this.sendButton.element.classList.add('send'); - this.sendButton.element.title = nls.localize('tweetFeedback', "Tweet Feedback"); + this.sendButton.element.title = localize('tweetFeedback', "Tweet Feedback"); disposables.add(attachButtonStyler(this.sendButton, this.themeService)); this.sendButton.onDidClick(() => this.onSubmit()); @@ -326,8 +327,8 @@ export class FeedbackWidget extends Disposable { private getCharCountText(charCount: number): string { const remaining = this.maxFeedbackCharacters - charCount; const text = (remaining === 1) - ? nls.localize("character left", "character left") - : nls.localize("characters left", "characters left"); + ? localize("character left", "character left") + : localize("characters left", "characters left"); return `(${remaining} ${text})`; } @@ -370,9 +371,9 @@ export class FeedbackWidget extends Disposable { } private invoke(element: HTMLElement, disposables: DisposableStore, callback: () => void): HTMLElement { - disposables.add(dom.addDisposableListener(element, 'click', callback)); + disposables.add(addDisposableListener(element, 'click', callback)); - disposables.add(dom.addDisposableListener(element, 'keypress', e => { + disposables.add(addDisposableListener(element, 'keypress', e => { if (e instanceof KeyboardEvent) { const keyboardEvent = <KeyboardEvent>e; if (keyboardEvent.keyCode === 13 || keyboardEvent.keyCode === 32) { // Enter or Spacebar diff --git a/src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts b/src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts index 630cd3a523a..b6317eb2dd7 100644 --- a/src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts +++ b/src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts @@ -60,6 +60,10 @@ export class FileEditorInput extends AbstractTextResourceEditorInput implements } } + if (!(capabilities & EditorInputCapabilities.Readonly)) { + capabilities |= EditorInputCapabilities.CanDropIntoEditor; + } + return capabilities; } diff --git a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts index f7eca9c59df..e77d66da61a 100644 --- a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts +++ b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts @@ -6,7 +6,7 @@ import { localize } from 'vs/nls'; import { assertIsDefined } from 'vs/base/common/types'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; -import { toAction } from 'vs/base/common/actions'; +import { IAction, toAction } from 'vs/base/common/actions'; import { VIEWLET_ID, TEXT_FILE_EDITOR_ID, BINARY_TEXT_FILE_MODE } from 'vs/workbench/contrib/files/common/files'; import { ITextFileService, TextFileOperationError, TextFileOperationResult } from 'vs/workbench/services/textfile/common/textfiles'; import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor'; @@ -26,7 +26,7 @@ import { ICodeEditorViewState, ScrollType } from 'vs/editor/common/editorCommon' import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IErrorWithActions } from 'vs/base/common/errorMessage'; +import { createErrorWithActions } from 'vs/base/common/errorMessage'; import { EditorActivation, ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IExplorerService } from 'vs/workbench/contrib/files/browser/files'; @@ -180,15 +180,29 @@ export class TextFileEditor extends BaseTextEditor<ICodeEditorViewState> { // Similar, handle case where we were asked to open a folder in the text editor. if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_IS_DIRECTORY) { - this.openAsFolder(input); + let action: IAction; + if (this.contextService.isInsideWorkspace(input.preferredResource)) { + action = toAction({ + id: 'workbench.files.action.reveal', label: localize('reveal', "Reveal in Explorer View"), run: async () => { + await this.paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar, true); - throw new Error(localize('openFolderError', "File is a directory")); + return this.explorerService.select(input.preferredResource, true); + } + }); + } else { + action = toAction({ + id: 'workbench.files.action.ok', label: localize('ok', "OK"), run: async () => { + // No operation possible, but clicking OK will close the editor + } + }); + } + + throw createErrorWithActions(new FileOperationError(localize('fileIsDirectoryError', "File is a directory"), FileOperationResult.FILE_IS_DIRECTORY), [action]); } // Offer to create a file from the error if we have a file not found and the name is valid if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND && await this.pathService.hasValidBasename(input.preferredResource)) { - const fileNotFoundError: FileOperationError & IErrorWithActions = new FileOperationError(localize('fileNotFoundError', "File not found"), FileOperationResult.FILE_NOT_FOUND); - fileNotFoundError.actions = [ + const fileNotFoundError = createErrorWithActions(new FileOperationError(localize('fileNotFoundError', "File not found"), FileOperationResult.FILE_NOT_FOUND), [ toAction({ id: 'workbench.files.action.createMissingFile', label: localize('createFile', "Create File"), run: async () => { await this.textFileService.create([{ resource: input.preferredResource }]); @@ -201,7 +215,7 @@ export class TextFileEditor extends BaseTextEditor<ICodeEditorViewState> { }); } }) - ]; + ]); throw fileNotFoundError; } @@ -262,22 +276,6 @@ export class TextFileEditor extends BaseTextEditor<ICodeEditorViewState> { group.openEditor(editor, editorOptions); } - private async openAsFolder(input: FileEditorInput): Promise<void> { - if (!this.group) { - return; - } - - // Since we cannot open a folder, we have to restore the previous input if any and close the editor - await this.group.closeEditor(this.input); - - // Best we can do is to reveal the folder in the explorer - if (this.contextService.isInsideWorkspace(input.preferredResource)) { - await this.paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar); - - this.explorerService.select(input.preferredResource, true); - } - } - override clearInput(): void { super.clearInput(); diff --git a/src/vs/workbench/contrib/files/browser/explorerService.ts b/src/vs/workbench/contrib/files/browser/explorerService.ts index 5293c2640ca..a1d5910f284 100644 --- a/src/vs/workbench/contrib/files/browser/explorerService.ts +++ b/src/vs/workbench/contrib/files/browser/explorerService.ts @@ -33,8 +33,7 @@ export class ExplorerService implements IExplorerService { private readonly disposables = new DisposableStore(); private editable: { stat: ExplorerItem; data: IEditableData } | undefined; - private _sortOrder: SortOrder; - private _lexicographicOptions: LexicographicOptions; + private config: IFilesConfiguration['explorer']; private cutItems: ExplorerItem[] | undefined; private view: IExplorerView | undefined; private model: ExplorerModel; @@ -52,8 +51,7 @@ export class ExplorerService implements IExplorerService { @IProgressService private readonly progressService: IProgressService, @IHostService hostService: IHostService ) { - this._sortOrder = this.configurationService.getValue('explorer.sortOrder'); - this._lexicographicOptions = this.configurationService.getValue('explorer.sortOrderLexicographicOptions'); + this.config = this.configurationService.getValue('explorer'); this.model = new ExplorerModel(this.contextService, this.uriIdentityService, this.fileService, this.configurationService); this.disposables.add(this.model); @@ -65,7 +63,7 @@ export class ExplorerService implements IExplorerService { // Filter to the ones we care const types = [FileChangeType.DELETED]; - if (this._sortOrder === SortOrder.Modified) { + if (this.config.sortOrder === SortOrder.Modified) { types.push(FileChangeType.UPDATED); } @@ -142,8 +140,8 @@ export class ExplorerService implements IExplorerService { get sortOrderConfiguration(): ISortOrderConfiguration { return { - sortOrder: this._sortOrder, - lexicographicOptions: this._lexicographicOptions, + sortOrder: this.config.sortOrder, + lexicographicOptions: this.config.sortOrderLexicographicOptions, }; } @@ -151,21 +149,19 @@ export class ExplorerService implements IExplorerService { this.view = contextProvider; } - getContext(respectMultiSelection: boolean, includeNestedChildren = false): ExplorerItem[] { + getContext(respectMultiSelection: boolean): ExplorerItem[] { if (!this.view) { return []; } const items = new Set<ExplorerItem>(this.view.getContext(respectMultiSelection)); - if (includeNestedChildren) { - items.forEach(item => { - if (item.nestedChildren) { - for (const child of item.nestedChildren) { - items.add(child); - } + items.forEach(item => { + if (this.view?.isItemCollapsed(item) && item.nestedChildren) { + for (const child of item.nestedChildren) { + items.add(child); } - }); - } + } + }); return [...items]; } @@ -261,7 +257,7 @@ export class ExplorerService implements IExplorerService { } // Stat needs to be resolved first and then revealed - const options: IResolveFileOptions = { resolveTo: [resource], resolveMetadata: this._sortOrder === SortOrder.Modified }; + const options: IResolveFileOptions = { resolveTo: [resource], resolveMetadata: this.config.sortOrder === SortOrder.Modified }; const root = this.findClosestRoot(resource); if (!root) { return undefined; @@ -302,6 +298,11 @@ export class ExplorerService implements IExplorerService { // File events private async onDidRunOperation(e: FileOperationEvent): Promise<void> { + // When nesting, changes to one file in a folder may impact the rendered structure + // of all the folder's immediate children, thus a recursive refresh is needed. + // Ideally the tree would be able to recusively refresh just one level but that does not yet exist. + const shouldDeepRefresh = this.config.fileNesting.enabled; + // Add if (e.isOperation(FileOperation.CREATE) || e.isOperation(FileOperation.COPY)) { const addedElement = e.target; @@ -313,7 +314,7 @@ export class ExplorerService implements IExplorerService { // Add the new file to its parent (Model) await Promise.all(parents.map(async p => { // We have to check if the parent is resolved #29177 - const resolveMetadata = this._sortOrder === `modified`; + const resolveMetadata = this.config.sortOrder === `modified`; if (!p.isDirectoryResolved) { const stat = await this.fileService.resolve(p.resource, { resolveMetadata }); if (stat) { @@ -327,7 +328,7 @@ export class ExplorerService implements IExplorerService { p.removeChild(childElement); p.addChild(childElement); // Refresh the Parent (View) - await this.view?.refresh(false, p); + await this.view?.refresh(shouldDeepRefresh, p); })); } } @@ -346,7 +347,7 @@ export class ExplorerService implements IExplorerService { await Promise.all(modelElements.map(async modelElement => { // Rename File (Model) modelElement.rename(newElement); - await this.view?.refresh(false, modelElement.parent); + await this.view?.refresh(shouldDeepRefresh, modelElement.parent); })); } @@ -363,7 +364,7 @@ export class ExplorerService implements IExplorerService { await this.view?.refresh(false, oldNestedParent); } await this.view?.refresh(false, oldParent); - await this.view?.refresh(false, newParents[index]); + await this.view?.refresh(shouldDeepRefresh, newParents[index]); })); } } @@ -384,7 +385,7 @@ export class ExplorerService implements IExplorerService { await this.view?.refresh(false, oldNestedParent); } // Refresh Parent (View) - await this.view?.refresh(false, parent); + await this.view?.refresh(shouldDeepRefresh, parent); } })); } @@ -393,22 +394,22 @@ export class ExplorerService implements IExplorerService { private async onConfigurationUpdated(configuration: IFilesConfiguration, event?: IConfigurationChangeEvent): Promise<void> { let shouldRefresh = false; - if (event?.affectedKeys.some(x => x.startsWith('explorer.experimental.fileNesting.'))) { + if (event?.affectedKeys.some(x => x.startsWith('explorer.fileNesting.'))) { shouldRefresh = true; } const configSortOrder = configuration?.explorer?.sortOrder || SortOrder.Default; - if (this._sortOrder !== configSortOrder) { - shouldRefresh = this._sortOrder !== undefined; - this._sortOrder = configSortOrder; + if (this.config.sortOrder !== configSortOrder) { + shouldRefresh = this.config.sortOrder !== undefined; } const configLexicographicOptions = configuration?.explorer?.sortOrderLexicographicOptions || LexicographicOptions.Default; - if (this._lexicographicOptions !== configLexicographicOptions) { - shouldRefresh = shouldRefresh || this._lexicographicOptions !== undefined; - this._lexicographicOptions = configLexicographicOptions; + if (this.config.sortOrderLexicographicOptions !== configLexicographicOptions) { + shouldRefresh = shouldRefresh || this.config.sortOrderLexicographicOptions !== undefined; } + this.config = configuration.explorer; + if (shouldRefresh) { await this.refresh(); } diff --git a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts index d88bd1c2d2b..57bb796dfc4 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts @@ -154,15 +154,16 @@ const copyRelativePathCommand = { // Editor Title Context Menu appendEditorTitleContextMenuItem(COPY_PATH_COMMAND_ID, copyPathCommand.title, ResourceContextKey.IsFileSystemResource, '1_cutcopypaste'); appendEditorTitleContextMenuItem(COPY_RELATIVE_PATH_COMMAND_ID, copyRelativePathCommand.title, ResourceContextKey.IsFileSystemResource, '1_cutcopypaste'); -appendEditorTitleContextMenuItem(REVEAL_IN_EXPLORER_COMMAND_ID, nls.localize('revealInSideBar', "Reveal in Explorer View"), ResourceContextKey.IsFileSystemResource); +appendEditorTitleContextMenuItem(REVEAL_IN_EXPLORER_COMMAND_ID, nls.localize('revealInSideBar', "Reveal in Explorer View"), ResourceContextKey.IsFileSystemResource, '2_files', 1); -export function appendEditorTitleContextMenuItem(id: string, title: string, when: ContextKeyExpression | undefined, group?: string): void { +export function appendEditorTitleContextMenuItem(id: string, title: string, when: ContextKeyExpression | undefined, group: string, order?: number): void { // Menu MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id, title }, when, - group: group || '2_files' + group, + order }); } @@ -576,7 +577,7 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { }); // Empty Editor Group Context Menu -MenuRegistry.appendMenuItem(MenuId.EmptyEditorGroupContext, { command: { id: NEW_UNTITLED_FILE_COMMAND_ID, title: nls.localize('newFile', "New File") }, group: '1_file', order: 10 }); +MenuRegistry.appendMenuItem(MenuId.EmptyEditorGroupContext, { command: { id: NEW_UNTITLED_FILE_COMMAND_ID, title: nls.localize('newFile', "New Text File") }, group: '1_file', order: 10 }); MenuRegistry.appendMenuItem(MenuId.EmptyEditorGroupContext, { command: { id: 'workbench.action.quickOpen', title: nls.localize('openFile', "Open File...") }, group: '1_file', order: 20 }); // File menu @@ -585,7 +586,7 @@ MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { group: '1_new', command: { id: NEW_UNTITLED_FILE_COMMAND_ID, - title: nls.localize({ key: 'miNewFile', comment: ['&& denotes a mnemonic'] }, "&&New File") + title: nls.localize({ key: 'miNewFile', comment: ['&& denotes a mnemonic'] }, "&&New Text File") }, order: 1 }); diff --git a/src/vs/workbench/contrib/files/browser/fileActions.ts b/src/vs/workbench/contrib/files/browser/fileActions.ts index d3100d24946..56160c6d47d 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.ts @@ -906,9 +906,7 @@ export const renameHandler = async (accessor: ServicesAccessor) => { export const moveFileToTrashHandler = async (accessor: ServicesAccessor) => { const explorerService = accessor.get(IExplorerService); - const configurationService = accessor.get(IConfigurationService); - const groupNests = configurationService.getValue<IFilesConfiguration>().explorer.experimental.fileNesting.operateAsGroup; - const stats = explorerService.getContext(true, groupNests).filter(s => !s.isRoot); + const stats = explorerService.getContext(true).filter(s => !s.isRoot); if (stats.length) { await deleteFiles(accessor.get(IExplorerService), accessor.get(IWorkingCopyFileService), accessor.get(IDialogService), accessor.get(IConfigurationService), stats, true); } @@ -916,9 +914,7 @@ export const moveFileToTrashHandler = async (accessor: ServicesAccessor) => { export const deleteFileHandler = async (accessor: ServicesAccessor) => { const explorerService = accessor.get(IExplorerService); - const configurationService = accessor.get(IConfigurationService); - const groupNests = configurationService.getValue<IFilesConfiguration>().explorer.experimental.fileNesting.operateAsGroup; - const stats = explorerService.getContext(true, groupNests).filter(s => !s.isRoot); + const stats = explorerService.getContext(true).filter(s => !s.isRoot); if (stats.length) { await deleteFiles(accessor.get(IExplorerService), accessor.get(IWorkingCopyFileService), accessor.get(IDialogService), accessor.get(IConfigurationService), stats, false); @@ -928,9 +924,7 @@ export const deleteFileHandler = async (accessor: ServicesAccessor) => { let pasteShouldMove = false; export const copyFileHandler = async (accessor: ServicesAccessor) => { const explorerService = accessor.get(IExplorerService); - const configurationService = accessor.get(IConfigurationService); - const groupNests = configurationService.getValue<IFilesConfiguration>().explorer.experimental.fileNesting.operateAsGroup; - const stats = explorerService.getContext(true, groupNests); + const stats = explorerService.getContext(true); if (stats.length > 0) { await explorerService.setToCopy(stats, false); pasteShouldMove = false; @@ -939,9 +933,7 @@ export const copyFileHandler = async (accessor: ServicesAccessor) => { export const cutFileHandler = async (accessor: ServicesAccessor) => { const explorerService = accessor.get(IExplorerService); - const configurationService = accessor.get(IConfigurationService); - const groupNests = configurationService.getValue<IFilesConfiguration>().explorer.experimental.fileNesting.operateAsGroup; - const stats = explorerService.getContext(true, groupNests); + const stats = explorerService.getContext(true); if (stats.length > 0) { await explorerService.setToCopy(stats, true); pasteShouldMove = true; diff --git a/src/vs/workbench/contrib/files/browser/fileImportExport.ts b/src/vs/workbench/contrib/files/browser/fileImportExport.ts index 08fdd59d803..71eda39201c 100644 --- a/src/vs/workbench/contrib/files/browser/fileImportExport.ts +++ b/src/vs/workbench/contrib/files/browser/fileImportExport.ts @@ -14,7 +14,7 @@ import { IFilesConfiguration, UndoConfirmLevel, VIEW_ID } from 'vs/workbench/con import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { Limiter, Promises, RunOnceWorker } from 'vs/base/common/async'; import { newWriteableBufferStream, VSBuffer } from 'vs/base/common/buffer'; -import { basename, joinPath } from 'vs/base/common/resources'; +import { basename, dirname, joinPath } from 'vs/base/common/resources'; import { ResourceFileEdit } from 'vs/editor/browser/services/bulkEditService'; import { ExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel'; import { URI } from 'vs/base/common/uri'; @@ -35,6 +35,7 @@ import { canceled } from 'vs/base/common/errors'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { WebFileSystemAccess } from 'vs/platform/files/browser/webFileSystemAccess'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; //#region Browser File Upload (drag and drop, input element) @@ -574,12 +575,15 @@ interface IDownloadOperation { export class FileDownload { + private static readonly LAST_USED_DOWNLOAD_PATH_STORAGE_KEY = 'workbench.explorer.downloadPath'; + constructor( @IFileService private readonly fileService: IFileService, @IExplorerService private readonly explorerService: IExplorerService, @IProgressService private readonly progressService: IProgressService, @ILogService private readonly logService: ILogService, - @IFileDialogService private readonly fileDialogService: IFileDialogService + @IFileDialogService private readonly fileDialogService: IFileDialogService, + @IStorageService private readonly storageService: IStorageService ) { } @@ -791,12 +795,18 @@ export class FileDownload { private async doDownloadNative(explorerItem: ExplorerItem, progress: IProgress<IProgressStep>, cts: CancellationTokenSource): Promise<void> { progress.report({ message: explorerItem.name }); - const defaultUri = joinPath( - explorerItem.isDirectory ? - await this.fileDialogService.defaultFolderPath(Schemas.file) : - await this.fileDialogService.defaultFilePath(Schemas.file), - explorerItem.name - ); + let defaultUri: URI; + const lastUsedDownloadPath = this.storageService.get(FileDownload.LAST_USED_DOWNLOAD_PATH_STORAGE_KEY, StorageScope.GLOBAL); + if (lastUsedDownloadPath) { + defaultUri = joinPath(URI.file(lastUsedDownloadPath), explorerItem.name); + } else { + defaultUri = joinPath( + explorerItem.isDirectory ? + await this.fileDialogService.defaultFolderPath(Schemas.file) : + await this.fileDialogService.defaultFilePath(Schemas.file), + explorerItem.name + ); + } const destination = await this.fileDialogService.showSaveDialog({ availableFileSystems: [Schemas.file], @@ -806,6 +816,11 @@ export class FileDownload { }); if (destination) { + + // Remember as last used download folder + this.storageService.store(FileDownload.LAST_USED_DOWNLOAD_PATH_STORAGE_KEY, dirname(destination).fsPath, StorageScope.GLOBAL, StorageTarget.MACHINE); + + // Perform download await this.explorerService.applyBulkEdit([new ResourceFileEdit(explorerItem.resource, destination, { overwrite: true, copy: true })], { undoLabel: localize('downloadBulkEdit', "Download {0}", explorerItem.name), progressLabel: localize('downloadingBulkEdit', "Downloading {0}", explorerItem.name), diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index 1a6f2c8f672..b162ad2ead5 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -246,7 +246,7 @@ configurationRegistry.registerConfiguration({ 'files.watcherExclude': { 'type': 'object', 'default': { '**/.git/objects/**': true, '**/.git/subtree-cache/**': true, '**/node_modules/*/**': true, '**/.hg/store/**': true }, - 'markdownDescription': nls.localize('watcherExclude', "Configure paths or glob patterns to exclude from file watching. Paths that are relative (for example `build/output`) will be resolved to an absolute path using the currently opened workspace. Glob patterns must match on absolute paths (i.e. prefix with `**/` or the full path and suffix with `/**` to match files within a path) to match properly (for example `**/build/output/**` or `/Users/name/workspaces/project/build/output/**`). When you experience the file watcher process consuming a lot of CPU, make sure to exclude large folders that are of less interest (such as build output folders)."), + 'markdownDescription': nls.localize('watcherExclude', "Configure paths or glob patterns to exclude from file watching. Paths or basic glob patterns that are relative (for example `build/output` or `*.js`) will be resolved to an absolute path using the currently opened workspace. Complex glob patterns must match on absolute paths (i.e. prefix with `**/` or the full path and suffix with `/**` to match files within a path) to match properly (for example `**/build/output/**` or `/Users/name/workspaces/project/build/output/**`). When you experience the file watcher process consuming a lot of CPU, make sure to exclude large folders that are of less interest (such as build output folders)."), 'scope': ConfigurationScope.RESOURCE }, 'files.watcherInclude': { @@ -337,10 +337,16 @@ configurationRegistry.registerConfiguration({ 'properties': { 'explorer.openEditors.visible': { 'type': 'number', - 'description': nls.localize({ key: 'openEditorsVisible', comment: ['Open is an adjective'] }, "Number of editors shown in the Open Editors pane. Setting this to 0 hides the Open Editors pane."), + 'description': nls.localize({ key: 'openEditorsVisible', comment: ['Open is an adjective'] }, "The maximum number of editors shown in the Open Editors pane. Setting this to 0 hides the Open Editors pane."), 'default': 9, 'minimum': 0 }, + 'explorer.openEditors.minVisible': { + 'type': 'number', + 'description': nls.localize({ key: 'openEditorsVisibleMin', comment: ['Open is an adjective'] }, "The minimum number of editor slots shown in the Open Editors pane. If set to 0 the Open Editors pane will dynamically resize based on the number of editors."), + 'default': 0, + 'minimum': 0 + }, 'explorer.openEditors.sortOrder': { 'type': 'string', 'enum': ['editorOrder', 'alphabetical', 'fullPath'], @@ -411,7 +417,7 @@ configurationRegistry.registerConfiguration({ nls.localize('sortOrder.modified', 'Files and folders are sorted by last modified date in descending order. Folders are displayed before files.'), nls.localize('sortOrder.foldersNestsFiles', 'Files and folders are sorted by their names. Folders are displayed before files. Files with nested children are displayed before other files.') ], - 'markdownDescription': nls.localize('sortOrder', "Controls the property-based sorting of files and folders in the explorer. When `#explorer.experimental.fileNesting.enabled#` is enabled, also controls sorting of nested files.") + 'markdownDescription': nls.localize('sortOrder', "Controls the property-based sorting of files and folders in the explorer. When `#explorer.fileNesting.enabled#` is enabled, also controls sorting of nested files.") }, 'explorer.sortOrderLexicographicOptions': { 'type': 'string', @@ -465,23 +471,18 @@ configurationRegistry.registerConfiguration({ 'description': nls.localize('copyRelativePathSeparator', "The path separation character used when copying relative file paths."), 'default': 'auto' }, - 'explorer.experimental.fileNesting.enabled': { + 'explorer.fileNesting.enabled': { 'type': 'boolean', scope: ConfigurationScope.RESOURCE, - 'markdownDescription': nls.localize('fileNestingEnabled', "Experimental. Controls whether file nesting is enabled in the explorer. File nesting allows for related files in a directory to be visually grouped together under a single parent file."), + 'markdownDescription': nls.localize('fileNestingEnabled', "Controls whether file nesting is enabled in the explorer. File nesting allows for related files in a directory to be visually grouped together under a single parent file."), 'default': false, }, - 'explorer.experimental.fileNesting.expand': { - 'type': 'boolean', - 'markdownDescription': nls.localize('fileNestingExpand', "Experimental. Controls whether file nests are automatically expanded. `#explorer.experimental.fileNesting.enabled#` must be set for this to take effect."), - 'default': true, - }, - 'explorer.experimental.fileNesting.operateAsGroup': { + 'explorer.fileNesting.expand': { 'type': 'boolean', - 'markdownDescription': nls.localize('operateAsGroup', "Controls whether file nests are treated as a group for clipboard operations, file deletions, and during drag and drop."), + 'markdownDescription': nls.localize('fileNestingExpand', "Controls whether file nests are automatically expanded. `#explorer.fileNesting.enabled#` must be set for this to take effect."), 'default': true, }, - 'explorer.experimental.fileNesting.patterns': { + 'explorer.fileNesting.patterns': { 'type': 'object', scope: ConfigurationScope.RESOURCE, 'markdownDescription': nls.localize('fileNestingPatterns', "Controls nesting of files in the explorer. Each __Item__ represents a parent pattern and may contain a single `*` character that matches any string. Each __Value__ represents a comma separated list of the child patterns that should be shown nested under a given parent. Child patterns may contain several special tokens:\n- `${capture}`: Matches the resolved value of the `*` from the parent pattern\n- `${basename}`: Matches the parent file's basename, the `file` in `file.ts`\n- `${extname}`: Matches the parent file's extension, the `ts` in `file.ts`\n- `${dirname}`: Matches the parent file's directory name, the `src` in `src/file.ts`\n- `*`: Matches any string, may only be used once per child pattern"), diff --git a/src/vs/workbench/contrib/files/browser/files.ts b/src/vs/workbench/contrib/files/browser/files.ts index 3c034c4e388..b4b01687527 100644 --- a/src/vs/workbench/contrib/files/browser/files.ts +++ b/src/vs/workbench/contrib/files/browser/files.ts @@ -56,6 +56,7 @@ export interface IExplorerView { itemsCopied(tats: ExplorerItem[], cut: boolean, previousCut: ExplorerItem[] | undefined): void; setEditable(stat: ExplorerItem, isEditing: boolean): Promise<void>; isItemVisible(item: ExplorerItem): boolean; + isItemCollapsed(item: ExplorerItem): boolean; hasFocus(): boolean; } diff --git a/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index c82cc35150a..1ba98e190c7 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -56,7 +56,6 @@ import { Codicon } from 'vs/base/common/codicons'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IEditorResolverService } from 'vs/workbench/services/editor/common/editorResolverService'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; -import { INotificationService } from 'vs/platform/notification/common/notification'; import { EditorOpenSource } from 'vs/platform/editor/common/editor'; import { ResourceMap } from 'vs/base/common/map'; @@ -188,7 +187,6 @@ export class ExplorerView extends ViewPane implements IExplorerView { @IMenuService private readonly menuService: IMenuService, @ITelemetryService telemetryService: ITelemetryService, @IExplorerService private readonly explorerService: IExplorerService, - @INotificationService private readonly notificationService: INotificationService, @IStorageService private readonly storageService: IStorageService, @IClipboardService private clipboardService: IClipboardService, @IFileService private readonly fileService: IFileService, @@ -325,6 +323,10 @@ export class ExplorerView extends ViewPane implements IExplorerView { return this.filter.filter(item, TreeVisibility.Visible); } + isItemCollapsed(item: ExplorerItem): boolean { + return this.tree.isCollapsed(item); + } + async setEditable(stat: ExplorerItem, isEditing: boolean): Promise<void> { if (isEditing) { this.horizontalScrolling = this.tree.options.horizontalScrolling; @@ -384,10 +386,10 @@ export class ExplorerView extends ViewPane implements IExplorerView { const isCompressionEnabled = () => this.configurationService.getValue<boolean>('explorer.compactFolders'); - const getFileNestingSettings = (item?: ExplorerItem) => this.configurationService.getValue<IFilesConfiguration>({ resource: item?.root.resource }).explorer.experimental.fileNesting; + const getFileNestingSettings = (item?: ExplorerItem) => this.configurationService.getValue<IFilesConfiguration>({ resource: item?.root.resource }).explorer.fileNesting; this.tree = <WorkbenchCompressibleAsyncDataTree<ExplorerItem | ExplorerItem[], ExplorerItem, FuzzyScore>>this.instantiationService.createInstance(WorkbenchCompressibleAsyncDataTree, 'FileExplorer', container, new ExplorerDelegate(), new ExplorerCompressionDelegate(), [this.renderer], - this.instantiationService.createInstance(ExplorerDataSource), { + this.instantiationService.createInstance(ExplorerDataSource, this.filter), { compressionEnabled: isCompressionEnabled(), accessibilityProvider: this.renderer, identityProvider, @@ -410,7 +412,7 @@ export class ExplorerView extends ViewPane implements IExplorerView { multipleSelectionSupport: true, filter: this.filter, sorter: this.instantiationService.createInstance(FileSorter), - dnd: this.instantiationService.createInstance(FileDragAndDrop), + dnd: this.instantiationService.createInstance(FileDragAndDrop, (item) => this.isItemCollapsed(item)), collapseByDefault: (e) => { if (e instanceof ExplorerItem) { if (e.hasNests && getFileNestingSettings(e).expand) { @@ -625,23 +627,9 @@ export class ExplorerView extends ViewPane implements IExplorerView { } const toRefresh = item || this.tree.getInput(); - if (this.configurationService.getValue<IFilesConfiguration>({ resource: item?.root.resource }).explorer.experimental.fileNesting.enabled) { - return (async () => { - try { - await this.tree.updateChildren(toRefresh, recursive, false, { - diffIdentityProvider: identityProvider - }); - } catch (e) { - this.notificationService.error('Internal error in file explorer. This may be due to experimental file nesting.'); - console.error('Unepxected error', e, 'in refreshing explorer. This may be due to experimental file nesting.'); - return; - } - })(); - } else { - return this.tree.updateChildren(toRefresh, recursive, false, { - diffIdentityProvider: identityProvider - }); - } + return this.tree.updateChildren(toRefresh, recursive, false, { + diffIdentityProvider: identityProvider + }); } override getOptimalWidth(): number { diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index f445df889f1..1181f9867c4 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -75,6 +75,7 @@ export const explorerRootErrorEmitter = new Emitter<URI>(); export class ExplorerDataSource implements IAsyncDataSource<ExplorerItem | ExplorerItem[], ExplorerItem> { constructor( + private fileFilter: FilesFilter, @IProgressService private readonly progressService: IProgressService, @IConfigurationService private readonly configService: IConfigurationService, @INotificationService private readonly notificationService: INotificationService, @@ -85,7 +86,8 @@ export class ExplorerDataSource implements IAsyncDataSource<ExplorerItem | Explo ) { } hasChildren(element: ExplorerItem | ExplorerItem[]): boolean { - return Array.isArray(element) || element.hasChildren; + // don't render nest parents as containing children when all the children are filtered out + return Array.isArray(element) || element.hasChildren((stat) => this.fileFilter.filter(stat, TreeVisibility.Visible)); } getChildren(element: ExplorerItem | ExplorerItem[]): ExplorerItem[] | Promise<ExplorerItem[]> { @@ -849,6 +851,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> { private dropEnabled = false; constructor( + private isCollapsed: (item: ExplorerItem) => boolean, @IExplorerService private explorerService: IExplorerService, @IEditorService private editorService: IEditorService, @IDialogService private dialogService: IDialogService, @@ -1082,8 +1085,8 @@ export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> { const elementsData = FileDragAndDrop.getStatsFromDragAndDropData(data); const distinctItems = new Set(elementsData); - if (this.configurationService.getValue<IFilesConfiguration>().explorer.experimental.fileNesting.operateAsGroup) { - for (const item of distinctItems) { + for (const item of distinctItems) { + if (this.isCollapsed(item)) { const nestedChildren = item.nestedChildren; if (nestedChildren) { for (const child of nestedChildren) { @@ -1092,6 +1095,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> { } } } + const items = distinctParents([...distinctItems], s => s.resource); const isCopy = (originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh); diff --git a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index b7bc23b3257..06ba5acdbde 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -59,6 +59,7 @@ const $ = dom.$; export class OpenEditorsView extends ViewPane { private static readonly DEFAULT_VISIBLE_OPEN_EDITORS = 9; + private static readonly DEFAULT_MIN_VISIBLE_OPEN_EDITORS = 0; static readonly ID = 'workbench.explorer.openEditorsView'; static readonly NAME = nls.localize({ key: 'openEditors', comment: ['Open is an adjective'] }, "Open Editors"); @@ -466,12 +467,17 @@ export class OpenEditorsView extends ViewPane { } private getMaxExpandedBodySize(): number { + let minVisibleOpenEditors = this.configurationService.getValue<number>('explorer.openEditors.minVisible'); + // If it's not a number setting it to 0 will result in dynamic resizing. + if (typeof minVisibleOpenEditors !== 'number') { + minVisibleOpenEditors = OpenEditorsView.DEFAULT_MIN_VISIBLE_OPEN_EDITORS; + } const containerModel = this.viewDescriptorService.getViewContainerModel(this.viewDescriptorService.getViewContainerByViewId(this.id)!)!; if (containerModel.visibleViewDescriptors.length <= 1) { return Number.POSITIVE_INFINITY; } - return this.elementCount * OpenEditorsDelegate.ITEM_HEIGHT; + return (Math.max(this.elementCount, minVisibleOpenEditors)) * OpenEditorsDelegate.ITEM_HEIGHT; } private getMinExpandedBodySize(): number { diff --git a/src/vs/workbench/contrib/files/common/explorerModel.ts b/src/vs/workbench/contrib/files/common/explorerModel.ts index dfc1c9effc5..4b7b2c6eace 100644 --- a/src/vs/workbench/contrib/files/common/explorerModel.ts +++ b/src/vs/workbench/contrib/files/common/explorerModel.ts @@ -120,8 +120,12 @@ export class ExplorerItem { this._isExcluded = value; } - get hasChildren() { - return this.isDirectory || this.hasNests; + hasChildren(filter: (stat: ExplorerItem) => boolean): boolean { + if (this.hasNests) { + return this.nestedChildren?.some(c => filter(c)) ?? false; + } else { + return this.isDirectory; + } } get hasNests() { @@ -298,7 +302,7 @@ export class ExplorerItem { } fetchChildren(sortOrder: SortOrder): ExplorerItem[] | Promise<ExplorerItem[]> { - const nestingConfig = this.configService.getValue<IFilesConfiguration>({ resource: this.root.resource }).explorer.experimental.fileNesting; + const nestingConfig = this.configService.getValue<IFilesConfiguration>({ resource: this.root.resource }).explorer.fileNesting; // fast path when the children can be resolved sync if (nestingConfig.enabled && this.nestedChildren) { @@ -369,12 +373,15 @@ export class ExplorerItem { private _fileNester: ExplorerFileNestingTrie | undefined; private get fileNester(): ExplorerFileNestingTrie { if (!this.root._fileNester) { - const nestingConfig = this.configService.getValue<IFilesConfiguration>({ resource: this.root.resource }).explorer.experimental.fileNesting; + const nestingConfig = this.configService.getValue<IFilesConfiguration>({ resource: this.root.resource }).explorer.fileNesting; const patterns = Object.entries(nestingConfig.patterns) .filter(entry => typeof (entry[0]) === 'string' && typeof (entry[1]) === 'string' && entry[0] && entry[1]) .map(([parentPattern, childrenPatterns]) => - [parentPattern.trim(), childrenPatterns.split(',').map(p => this.getPlatformAwareName(p.trim().replace(/\u200b/g, '')))] as [string, string[]]); + [ + this.getPlatformAwareName(parentPattern.trim()), + childrenPatterns.split(',').map(p => this.getPlatformAwareName(p.trim().replace(/\u200b/g, ''))) + ] as [string, string[]]); this.root._fileNester = new ExplorerFileNestingTrie(patterns); } diff --git a/src/vs/workbench/contrib/files/common/files.ts b/src/vs/workbench/contrib/files/common/files.ts index aff70bc0e8b..5f10b350de4 100644 --- a/src/vs/workbench/contrib/files/common/files.ts +++ b/src/vs/workbench/contrib/files/common/files.ts @@ -98,13 +98,10 @@ export interface IFilesConfiguration extends PlatformIFilesConfiguration, IWorkb badges: boolean; }; incrementalNaming: 'simple' | 'smart'; - experimental: { - fileNesting: { - enabled: boolean; - operateAsGroup: boolean; - expand: boolean; - patterns: { [parent: string]: string }; - }; + fileNesting: { + enabled: boolean; + expand: boolean; + patterns: { [parent: string]: string }; }; }; editor: IEditorOptions; diff --git a/src/vs/workbench/contrib/files/electron-sandbox/fileActions.contribution.ts b/src/vs/workbench/contrib/files/electron-sandbox/fileActions.contribution.ts index 7e5450675c1..6473ad59136 100644 --- a/src/vs/workbench/contrib/files/electron-sandbox/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/electron-sandbox/fileActions.contribution.ts @@ -57,7 +57,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } }); -appendEditorTitleContextMenuItem(REVEAL_IN_OS_COMMAND_ID, REVEAL_IN_OS_LABEL, REVEAL_IN_OS_WHEN_CONTEXT); +appendEditorTitleContextMenuItem(REVEAL_IN_OS_COMMAND_ID, REVEAL_IN_OS_LABEL, REVEAL_IN_OS_WHEN_CONTEXT, '2_files', 0); // Menu registration - open editors diff --git a/src/vs/workbench/contrib/files/electron-sandbox/textFileEditor.ts b/src/vs/workbench/contrib/files/electron-sandbox/textFileEditor.ts index e4d1eee0b33..64978ce1742 100644 --- a/src/vs/workbench/contrib/files/electron-sandbox/textFileEditor.ts +++ b/src/vs/workbench/contrib/files/electron-sandbox/textFileEditor.ts @@ -62,24 +62,22 @@ export class NativeTextFileEditor extends TextFileEditor { if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_EXCEEDS_MEMORY_LIMIT) { const memoryLimit = Math.max(MIN_MAX_MEMORY_SIZE_MB, +this.textResourceConfigurationService.getValue<number>(undefined, 'files.maxMemoryForLargeFilesMB') || FALLBACK_MAX_MEMORY_SIZE_MB); - throw createErrorWithActions(localize('fileTooLargeForHeapError', "To open a file of this size, you need to restart and allow {0} to use more memory", this.productService.nameShort), { - actions: [ - toAction({ - id: 'workbench.window.action.relaunchWithIncreasedMemoryLimit', label: localize('relaunchWithIncreasedMemoryLimit', "Restart with {0} MB", memoryLimit), run: () => { - return this.nativeHostService.relaunch({ - addArgs: [ - `--max-memory=${memoryLimit}` - ] - }); - } - }), - toAction({ - id: 'workbench.window.action.configureMemoryLimit', label: localize('configureMemoryLimit', 'Configure Memory Limit'), run: () => { - return this.preferencesService.openUserSettings({ query: 'files.maxMemoryForLargeFilesMB' }); - } - }), - ] - }); + throw createErrorWithActions(localize('fileTooLargeForHeapError', "To open a file of this size, you need to restart and allow {0} to use more memory", this.productService.nameShort), [ + toAction({ + id: 'workbench.window.action.relaunchWithIncreasedMemoryLimit', label: localize('relaunchWithIncreasedMemoryLimit', "Restart with {0} MB", memoryLimit), run: () => { + return this.nativeHostService.relaunch({ + addArgs: [ + `--max-memory=${memoryLimit}` + ] + }); + } + }), + toAction({ + id: 'workbench.window.action.configureMemoryLimit', label: localize('configureMemoryLimit', 'Configure Memory Limit'), run: () => { + return this.preferencesService.openUserSettings({ query: 'files.maxMemoryForLargeFilesMB' }); + } + }), + ]); } // Fallback to handling in super type diff --git a/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts b/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts index 3481452d194..aba0c2eed69 100644 --- a/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts +++ b/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts @@ -3,15 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { EditorAction, registerEditorAction, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { getCodeEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorAction, registerEditorAction } from 'vs/editor/browser/editorExtensions'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { DocumentFormattingEditProvider, DocumentRangeFormattingEditProvider } from 'vs/editor/common/languages'; import * as nls from 'vs/nls'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { formatDocumentRangesWithProvider, formatDocumentWithProvider, getRealAndSyntheticDocumentFormattersOrdered, FormattingConflicts, FormattingMode } from 'vs/editor/contrib/format/browser/format'; import { Range } from 'vs/editor/common/core/range'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -21,7 +21,7 @@ import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'v import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry, IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IExtensionService, toExtension } from 'vs/workbench/services/extensions/common/extensions'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITextModel } from 'vs/editor/common/model'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; @@ -30,6 +30,10 @@ import { IWorkbenchExtensionEnablementService } from 'vs/workbench/services/exte import { editorConfigurationBaseNode } from 'vs/editor/common/config/editorConfigurationSchema'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import { ILanguageStatusService } from 'vs/workbench/services/languageStatus/common/languageStatusService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { generateUuid } from 'vs/base/common/uuid'; type FormattingEditProvider = DocumentFormattingEditProvider | DocumentRangeFormattingEditProvider; @@ -41,6 +45,8 @@ class DefaultFormatter extends Disposable implements IWorkbenchContribution { static extensionItemLabels: string[] = []; static extensionDescriptions: string[] = []; + private readonly _languageStatusStore = this._store.add(new DisposableStore()); + constructor( @IExtensionService private readonly _extensionService: IExtensionService, @IWorkbenchExtensionEnablementService private readonly _extensionEnablementService: IWorkbenchExtensionEnablementService, @@ -49,10 +55,17 @@ class DefaultFormatter extends Disposable implements IWorkbenchContribution { @IDialogService private readonly _dialogService: IDialogService, @IQuickInputService private readonly _quickInputService: IQuickInputService, @ILanguageService private readonly _languageService: ILanguageService, + @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, + @ILanguageStatusService private readonly _languageStatusService: ILanguageStatusService, + @IEditorService private readonly _editorService: IEditorService, ) { super(); - this._register(this._extensionService.onDidChangeExtensions(this._updateConfigValues, this)); - this._register(FormattingConflicts.setFormatterSelector((formatter, document, mode) => this._selectFormatter(formatter, document, mode))); + this._store.add(this._extensionService.onDidChangeExtensions(this._updateConfigValues, this)); + this._store.add(FormattingConflicts.setFormatterSelector((formatter, document, mode) => this._selectFormatter(formatter, document, mode))); + this._store.add(_editorService.onDidActiveEditorChange(this._updateStatus, this)); + this._store.add(_languageFeaturesService.documentFormattingEditProvider.onDidChange(this._updateStatus, this)); + this._store.add(_languageFeaturesService.documentRangeFormattingEditProvider.onDidChange(this._updateStatus, this)); + this._store.add(_configService.onDidChangeConfiguration(e => e.affectsConfiguration(DefaultFormatter.configName) && this._updateStatus())); this._updateConfigValues(); } @@ -93,8 +106,7 @@ class DefaultFormatter extends Disposable implements IWorkbenchContribution { return s.match(/\s/) ? `'${s}'` : s; } - private async _selectFormatter<T extends FormattingEditProvider>(formatter: T[], document: ITextModel, mode: FormattingMode): Promise<T | undefined> { - + private async _analyzeFormatter<T extends FormattingEditProvider>(formatter: T[], document: ITextModel): Promise<T | string> { const defaultFormatterId = this._configService.getValue<string>(DefaultFormatter.configName, { resource: document.uri, overrideIdentifier: document.getLanguageId() @@ -114,23 +126,9 @@ class DefaultFormatter extends Disposable implements IWorkbenchContribution { // formatter does not target this file const langName = this._languageService.getLanguageName(document.getLanguageId()) || document.getLanguageId(); const detail = nls.localize('miss', "Extension '{0}' is configured as formatter but it cannot format '{1}'-files", extension.displayName || extension.name, langName); - if (mode === FormattingMode.Silent) { - this._notificationService.status(detail, { hideAfter: 4000 }); - return undefined; - } else { - const result = await this._dialogService.confirm({ - message: nls.localize('miss.1', "Change Default Formatter"), - detail, - primaryButton: nls.localize('do.config', "Configure..."), - secondaryButton: nls.localize('cancel', "Cancel") - }); - if (result.confirmed) { - return this._pickAndPersistDefaultFormatter(formatter, document); - } else { - return undefined; - } - } + return detail; } + } else if (formatter.length === 1) { // ok -> nothing configured but only one formatter available return formatter[0]; @@ -138,26 +136,35 @@ class DefaultFormatter extends Disposable implements IWorkbenchContribution { const langName = this._languageService.getLanguageName(document.getLanguageId()) || document.getLanguageId(); const message = !defaultFormatterId - ? nls.localize('config.needed', "There are multiple formatters for '{0}' files. Select a default formatter to continue.", DefaultFormatter._maybeQuotes(langName)) + ? nls.localize('config.needed', "There are multiple formatters for '{0}' files. One of them should be configured as default formatter.", DefaultFormatter._maybeQuotes(langName)) : nls.localize('config.bad', "Extension '{0}' is configured as formatter but not available. Select a different default formatter to continue.", defaultFormatterId); + return message; + } + + private async _selectFormatter<T extends FormattingEditProvider>(formatter: T[], document: ITextModel, mode: FormattingMode): Promise<T | undefined> { + const formatterOrMessage = await this._analyzeFormatter(formatter, document); + if (typeof formatterOrMessage !== 'string') { + return formatterOrMessage; + } + if (mode !== FormattingMode.Silent) { // running from a user action -> show modal dialog so that users configure // a default formatter const result = await this._dialogService.confirm({ - message, + message: nls.localize('miss.1', "Configure Default Formatter"), + detail: formatterOrMessage, primaryButton: nls.localize('do.config', "Configure..."), secondaryButton: nls.localize('cancel', "Cancel") }); if (result.confirmed) { return this._pickAndPersistDefaultFormatter(formatter, document); } - } else { // no user action -> show a silent notification and proceed this._notificationService.prompt( Severity.Info, - message, + formatterOrMessage, [{ label: nls.localize('do.config', "Configure..."), run: () => this._pickAndPersistDefaultFormatter(formatter, document) }], { silent: true } ); @@ -184,6 +191,51 @@ class DefaultFormatter extends Disposable implements IWorkbenchContribution { }); return formatter[pick.index]; } + + // --- status item + + private _updateStatus() { + this._languageStatusStore.clear(); + + const editor = getCodeEditor(this._editorService.activeTextEditorControl); + if (!editor || !editor.hasModel()) { + return; + } + + + const document = editor.getModel(); + const formatter = getRealAndSyntheticDocumentFormattersOrdered(this._languageFeaturesService.documentFormattingEditProvider, this._languageFeaturesService.documentRangeFormattingEditProvider, document); + + if (formatter.length === 0) { + return; + } + + const cts = new CancellationTokenSource(); + this._languageStatusStore.add(toDisposable(() => cts.dispose(true))); + + this._analyzeFormatter(formatter, document).then(result => { + if (cts.token.isCancellationRequested) { + return; + } + if (typeof result !== 'string') { + return; + } + const command = { id: `formatter/configure/dfl/${generateUuid()}`, title: nls.localize('do.config', "Configure...") }; + this._languageStatusStore.add(CommandsRegistry.registerCommand(command.id, () => this._pickAndPersistDefaultFormatter(formatter, document))); + this._languageStatusStore.add(this._languageStatusService.addStatus({ + id: 'formatter.conflict', + name: nls.localize('summary', "Formatter Conflicts"), + selector: { language: document.getLanguageId(), pattern: document.uri.fsPath }, + severity: Severity.Error, + label: nls.localize('formatter', "Formatting"), + detail: result, + busy: false, + source: '', + command, + accessibilityInfo: undefined + })); + }); + } } Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution( diff --git a/src/vs/workbench/contrib/keybindings/browser/keybindings.contribution.ts b/src/vs/workbench/contrib/keybindings/browser/keybindings.contribution.ts index 6fc249b03be..5f6cdfb3298 100644 --- a/src/vs/workbench/contrib/keybindings/browser/keybindings.contribution.ts +++ b/src/vs/workbench/contrib/keybindings/browser/keybindings.contribution.ts @@ -9,7 +9,7 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { CATEGORIES } from 'vs/workbench/common/actions'; import { rendererLogChannelId } from 'vs/workbench/contrib/logs/common/logConstants'; -import { IOutputService } from 'vs/workbench/contrib/output/common/output'; +import { IOutputService } from 'vs/workbench/services/output/common/output'; class ToggleKeybindingsLogAction extends Action2 { diff --git a/src/vs/workbench/contrib/languageDetection/browser/languageDetection.contribution.ts b/src/vs/workbench/contrib/languageDetection/browser/languageDetection.contribution.ts new file mode 100644 index 00000000000..94f1e9af9bd --- /dev/null +++ b/src/vs/workbench/contrib/languageDetection/browser/languageDetection.contribution.ts @@ -0,0 +1,148 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { getCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { localize } from 'vs/nls'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from 'vs/workbench/services/statusbar/browser/statusbar'; +import { ILanguageDetectionService } from 'vs/workbench/services/languageDetection/common/languageDetectionWorkerService'; +import { ThrottledDelayer } from 'vs/base/common/async'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { registerAction2, Action2 } from 'vs/platform/actions/common/actions'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { NOTEBOOK_EDITOR_EDITABLE } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { Schemas } from 'vs/base/common/network'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; + +const detectLanguageCommandId = 'editor.detectLanguage'; + +class LanguageDetectionStatusContribution implements IWorkbenchContribution { + + private static readonly _id = 'status.languageDetectionStatus'; + + private readonly _disposables = new DisposableStore(); + private _combinedEntry?: IStatusbarEntryAccessor; + private _delayer = new ThrottledDelayer(1000); + private _renderDisposables = new DisposableStore(); + + constructor( + @ILanguageDetectionService private readonly _languageDetectionService: ILanguageDetectionService, + @IStatusbarService private readonly _statusBarService: IStatusbarService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IEditorService private readonly _editorService: IEditorService, + @ILanguageService private readonly _languageService: ILanguageService, + @IKeybindingService private readonly _keybindingService: IKeybindingService, + ) { + _editorService.onDidActiveEditorChange(() => this._update(true), this, this._disposables); + this._update(false); + } + + dispose(): void { + this._disposables.dispose(); + this._delayer.dispose(); + this._combinedEntry?.dispose(); + this._renderDisposables.dispose(); + } + + private _update(clear: boolean): void { + if (clear) { + this._combinedEntry?.dispose(); + this._combinedEntry = undefined; + } + this._delayer.trigger(() => this._doUpdate()); + } + + private async _doUpdate(): Promise<void> { + const editor = getCodeEditor(this._editorService.activeTextEditorControl); + + this._renderDisposables.clear(); + + // update when editor language changes + editor?.onDidChangeModelLanguage(() => this._update(true), this, this._renderDisposables); + editor?.onDidChangeModelContent(() => this._update(false), this, this._renderDisposables); + const editorModel = editor?.getModel(); + const editorUri = editorModel?.uri; + const existingId = editorModel?.getLanguageId(); + const enablementConfig = this._configurationService.getValue('workbench.editor.languageDetectionHints'); + const enabled = enablementConfig === 'always' || enablementConfig === 'textEditors'; + const disableLightbulb = !enabled || editorUri?.scheme !== Schemas.untitled || !existingId; + + if (disableLightbulb || !editorUri) { + this._combinedEntry?.dispose(); + this._combinedEntry = undefined; + } else { + const lang = await this._languageDetectionService.detectLanguage(editorUri); + const skip: Record<string, string | undefined> = { 'jsonc': 'json' }; + const existing = editorModel.getLanguageId(); + if (lang && lang !== existing && skip[existing] !== lang) { + const detectedName = this._languageService.getLanguageName(lang) || lang; + let tooltip = localize('status.autoDetectLanguage', "Accept Detected Language: {0}", detectedName); + const keybinding = this._keybindingService.lookupKeybinding(detectLanguageCommandId); + const label = keybinding?.getLabel(); + if (label) { + tooltip += ` (${label})`; + } + + const props: IStatusbarEntry = { + name: localize('langDetection.name', "Language Detection"), + ariaLabel: localize('langDetection.aria', "Change to Detected Language: {0}", lang), + tooltip, + command: detectLanguageCommandId, + text: '$(lightbulb-autofix)', + }; + if (!this._combinedEntry) { + this._combinedEntry = this._statusBarService.addEntry(props, LanguageDetectionStatusContribution._id, StatusbarAlignment.RIGHT, { id: 'status.editor.mode', alignment: StatusbarAlignment.RIGHT, compact: true }); + } else { + this._combinedEntry.update(props); + } + } else { + this._combinedEntry?.dispose(); + this._combinedEntry = undefined; + } + } + } +} + +Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(LanguageDetectionStatusContribution, LifecyclePhase.Restored); + + +registerAction2(class extends Action2 { + + constructor() { + super({ + id: detectLanguageCommandId, + title: localize('detectlang', 'Detect Language from Content'), + f1: true, + precondition: ContextKeyExpr.and(NOTEBOOK_EDITOR_EDITABLE.toNegated(), EditorContextKeys.editorTextFocus), + keybinding: { primary: KeyCode.KeyD | KeyMod.Alt | KeyMod.Shift, weight: KeybindingWeight.WorkbenchContrib } + }); + } + + async run(accessor: ServicesAccessor): Promise<void> { + const editorService = accessor.get(IEditorService); + const languageDetectionService = accessor.get(ILanguageDetectionService); + const editor = getCodeEditor(editorService.activeTextEditorControl); + const notificationService = accessor.get(INotificationService); + const editorUri = editor?.getModel()?.uri; + if (editorUri) { + const lang = await languageDetectionService.detectLanguage(editorUri); + if (lang) { + editor.getModel()?.setMode(lang); + } else { + notificationService.warn(localize('noDetection', "Unable to detect editor language")); + } + } + } +}); diff --git a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts index d6d4689d90e..0372c278593 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts +++ b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/languageStatus'; import * as dom from 'vs/base/browser/dom'; import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; -import { DisposableStore, dispose } from 'vs/base/common/lifecycle'; +import { DisposableStore, dispose, toDisposable } from 'vs/base/common/lifecycle'; import Severity from 'vs/base/common/severity'; import { getCodeEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { localize } from 'vs/nls'; @@ -28,6 +28,8 @@ import { Codicon } from 'vs/base/common/codicons'; import { IStorageService, IStorageValueChangeEvent, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { equals } from 'vs/base/common/arrays'; import { URI } from 'vs/base/common/uri'; +import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; class LanguageStatusViewModel { @@ -202,16 +204,21 @@ class EditorStatusContribution implements IWorkbenchContribution { // animate the status bar icon whenever language status changes, repeat animation // when severity is warning or error, don't show animation when showing progress/busy const userHasInteractedWithStatus = this._interactionCounter.value >= 3; - const node = document.querySelector('.monaco-workbench .statusbar DIV#status\\.languageStatus span.codicon'); - if (node instanceof HTMLElement) { + const node = document.querySelector('.monaco-workbench .statusbar DIV#status\\.languageStatus A>SPAN.codicon'); + const container = document.querySelector('.monaco-workbench .statusbar DIV#status\\.languageStatus'); + if (node instanceof HTMLElement && container) { const _wiggle = 'wiggle'; - const _repeat = 'repeat'; + const _flash = 'flash'; if (!isOneBusy) { + // wiggle icon when severe or "new" node.classList.toggle(_wiggle, showSeverity || !userHasInteractedWithStatus); - node.classList.toggle(_repeat, showSeverity); - this._renderDisposables.add(dom.addDisposableListener(node, 'animationend', _e => node.classList.remove(_wiggle, _repeat))); + this._renderDisposables.add(dom.addDisposableListener(node, 'animationend', _e => node.classList.remove(_wiggle))); + // flash background when severe + container.classList.toggle(_flash, showSeverity); + this._renderDisposables.add(dom.addDisposableListener(container, 'animationend', _e => container.classList.remove(_flash))); } else { - node.classList.remove(_wiggle, _repeat); + node.classList.remove(_wiggle); + container.classList.remove(_flash); } } @@ -226,7 +233,8 @@ class EditorStatusContribution implements IWorkbenchContribution { observer.disconnect(); } }); - observer.observe(document.body, { childList: true, subtree: true }); + observer.observe(hoverTarget, { childList: true, subtree: true }); + this._renderDisposables.add(toDisposable(() => observer.disconnect())); } } } @@ -384,3 +392,19 @@ class EditorStatusContribution implements IWorkbenchContribution { } Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(EditorStatusContribution, LifecyclePhase.Restored); + +registerAction2(class extends Action2 { + + constructor() { + super({ + id: 'editor.inlayHints.Reset', + title: localize('reset', 'Reset Language Status Interaction Counter'), + category: localize('cat', 'View'), + f1: true + }); + } + + run(accessor: ServicesAccessor): void { + accessor.get(IStorageService).remove('languageStatus.interactCount', StorageScope.GLOBAL); + } +}); diff --git a/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css b/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css index 131695f460a..1fc68790f97 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css +++ b/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css @@ -3,37 +3,56 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +/* status bar animation */ @keyframes wiggle { 0% { - transform: rotate(0) scale(1) + transform: rotate(0) scale(1); } 15%, 45% { - transform: rotate(.04turn) scale(1.1) + transform: rotate(.04turn) scale(1.1); } 30%, 60% { - transform: rotate(-.04turn) scale(1.2) + transform: rotate(-.04turn) scale(1.2); } 100% { - transform: rotate(0) scale(1) + transform: rotate(0) scale(1); } } -.monaco-workbench.enable-motion .statusbar DIV#status\.languageStatus span.codicon.wiggle { +.monaco-workbench .statusbar DIV#status\.languageStatus A>SPAN.codicon.wiggle { animation-duration: .8s; animation-iteration-count: 1; animation-name: wiggle; } -.monaco-workbench.enable-motion .statusbar DIV#status\.languageStatus span.codicon.wiggle.repeat { - animation-iteration-count: 3; +@keyframes flash { + 0% { + background-color: initial; + } + + 50% { + background-color: var(--vscode-statusBarItem-prominentBackground); + } + + 100% { + background-color: initial; + } +} + +.monaco-workbench .statusbar DIV#status\.languageStatus.flash A { + animation-duration: .8s; + animation-iteration-count: 1; + animation-name: flash; } +/* --- hover */ + .monaco-workbench .hover-language-status { display: flex; padding: 4px 8px; diff --git a/src/vs/workbench/contrib/localHistory/browser/localHistory.ts b/src/vs/workbench/contrib/localHistory/browser/localHistory.ts index c182f0ccfd1..20bd201237b 100644 --- a/src/vs/workbench/contrib/localHistory/browser/localHistory.ts +++ b/src/vs/workbench/contrib/localHistory/browser/localHistory.ts @@ -8,8 +8,22 @@ import { Codicon } from 'vs/base/common/codicons'; import { language } from 'vs/base/common/platform'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; +import { IdleValue } from 'vs/base/common/async'; -export const LOCAL_HISTORY_DATE_FORMATTER = new Intl.DateTimeFormat(language, { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' }); +export const LOCAL_HISTORY_DATE_FORMATTER: IdleValue<{ format: (timestamp: number) => string }> = new IdleValue(() => { + const options: Intl.DateTimeFormatOptions = { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' }; + + let formatter: Intl.DateTimeFormat; + try { + formatter = new Intl.DateTimeFormat(language, options); + } catch (error) { + formatter = new Intl.DateTimeFormat(undefined, options); // error can happen when language is invalid (https://github.com/microsoft/vscode/issues/147086) + } + + return { + format: date => formatter.format(date) + }; +}); export const LOCAL_HISTORY_MENU_CONTEXT_VALUE = 'localHistory:item'; export const LOCAL_HISTORY_MENU_CONTEXT_KEY = ContextKeyExpr.equals('timelineItem', LOCAL_HISTORY_MENU_CONTEXT_VALUE); diff --git a/src/vs/workbench/contrib/localHistory/browser/localHistoryCommands.ts b/src/vs/workbench/contrib/localHistory/browser/localHistoryCommands.ts index 0b2a709a069..49a5a67e8e7 100644 --- a/src/vs/workbench/contrib/localHistory/browser/localHistoryCommands.ts +++ b/src/vs/workbench/contrib/localHistory/browser/localHistoryCommands.ts @@ -632,7 +632,7 @@ export async function findLocalHistoryEntry(workingCopyHistoryService: IWorkingC const SEP = /\//g; function toLocalHistoryEntryDateLabel(timestamp: number): string { - return `${LOCAL_HISTORY_DATE_FORMATTER.format(timestamp).replace(SEP, '-')}`; // preserving `/` will break editor labels, so replace it with a non-path symbol + return `${LOCAL_HISTORY_DATE_FORMATTER.value.format(timestamp).replace(SEP, '-')}`; // preserving `/` will break editor labels, so replace it with a non-path symbol } //#endregion diff --git a/src/vs/workbench/contrib/localHistory/browser/localHistoryTimeline.ts b/src/vs/workbench/contrib/localHistory/browser/localHistoryTimeline.ts index ca34fae943c..2dee3866cbd 100644 --- a/src/vs/workbench/contrib/localHistory/browser/localHistoryTimeline.ts +++ b/src/vs/workbench/contrib/localHistory/browser/localHistoryTimeline.ts @@ -8,7 +8,7 @@ import { Emitter } from 'vs/base/common/event'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { InternalTimelineOptions, ITimelineService, Timeline, TimelineChangeEvent, TimelineItem, TimelineOptions, TimelineProvider } from 'vs/workbench/contrib/timeline/common/timeline'; +import { ITimelineService, Timeline, TimelineChangeEvent, TimelineItem, TimelineOptions, TimelineProvider } from 'vs/workbench/contrib/timeline/common/timeline'; import { IWorkingCopyHistoryEntry, IWorkingCopyHistoryService } from 'vs/workbench/services/workingCopy/common/workingCopyHistory'; import { URI } from 'vs/base/common/uri'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; @@ -102,7 +102,7 @@ export class LocalHistoryTimeline extends Disposable implements IWorkbenchContri }); } - async provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken, internalOptions?: InternalTimelineOptions): Promise<Timeline> { + async provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken): Promise<Timeline> { const items: TimelineItem[] = []; // Try to convert the provided `uri` into a form that is likely @@ -151,7 +151,7 @@ export class LocalHistoryTimeline extends Disposable implements IWorkbenchContri return { handle: entry.id, label: SaveSourceRegistry.getSourceLabel(entry.source), - tooltip: new MarkdownString(`$(history) ${LOCAL_HISTORY_DATE_FORMATTER.format(entry.timestamp)}\n\n${SaveSourceRegistry.getSourceLabel(entry.source)}`, { supportThemeIcons: true }), + tooltip: new MarkdownString(`$(history) ${LOCAL_HISTORY_DATE_FORMATTER.value.format(entry.timestamp)}\n\n${SaveSourceRegistry.getSourceLabel(entry.source)}`, { supportThemeIcons: true }), source: LocalHistoryTimeline.ID, timestamp: entry.timestamp, themeIcon: LOCAL_HISTORY_ICON_ENTRY, diff --git a/src/vs/workbench/contrib/logs/browser/logs.contribution.ts b/src/vs/workbench/contrib/logs/browser/logs.contribution.ts new file mode 100644 index 00000000000..d78954e856e --- /dev/null +++ b/src/vs/workbench/contrib/logs/browser/logs.contribution.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 { Registry } from 'vs/platform/registry/common/platform'; +import { IWorkbenchActionRegistry, Extensions as WorkbenchActionExtensions, CATEGORIES } from 'vs/workbench/common/actions'; +import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { OpenWindowSessionLogFileAction } from 'vs/workbench/contrib/logs/common/logsActions'; +import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { LogsDataCleaner } from 'vs/workbench/contrib/logs/common/logsDataCleaner'; + +class WebLogOutputChannels extends Disposable implements IWorkbenchContribution { + + constructor( + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super(); + this.registerWebContributions(); + } + + private registerWebContributions(): void { + this.instantiationService.createInstance(LogsDataCleaner); + + const workbenchActionsRegistry = Registry.as<IWorkbenchActionRegistry>(WorkbenchActionExtensions.WorkbenchActions); + workbenchActionsRegistry.registerWorkbenchAction(SyncActionDescriptor.from(OpenWindowSessionLogFileAction), 'Developer: Open Window Log File (Session)...', CATEGORIES.Developer.value); + } + +} + +Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(WebLogOutputChannels, LifecyclePhase.Restored); diff --git a/src/vs/workbench/contrib/logs/common/logs.contribution.ts b/src/vs/workbench/contrib/logs/common/logs.contribution.ts index 6cbfdc4cd7b..5e533149305 100644 --- a/src/vs/workbench/contrib/logs/common/logs.contribution.ts +++ b/src/vs/workbench/contrib/logs/common/logs.contribution.ts @@ -4,29 +4,22 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import { join } from 'vs/base/common/path'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchActionRegistry, Extensions as WorkbenchActionExtensions, CATEGORIES } from 'vs/workbench/common/actions'; import { Action2, registerAction2, SyncActionDescriptor } from 'vs/platform/actions/common/actions'; -import { SetLogLevelAction, OpenWindowSessionLogFileAction } from 'vs/workbench/contrib/logs/common/logsActions'; +import { SetLogLevelAction } from 'vs/workbench/contrib/logs/common/logsActions'; import * as Constants from 'vs/workbench/contrib/logs/common/logConstants'; import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { IFileService, whenProviderRegistered } from 'vs/platform/files/common/files'; -import { URI } from 'vs/base/common/uri'; -import { IOutputChannelRegistry, Extensions as OutputExt } from 'vs/workbench/services/output/common/output'; +import { IFileService } from 'vs/platform/files/common/files'; +import { IOutputService, registerLogChannel } from 'vs/workbench/services/output/common/output'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { ILogService, LogLevel } from 'vs/platform/log/common/log'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { isWeb } from 'vs/base/common/platform'; -import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { LogsDataCleaner } from 'vs/workbench/contrib/logs/common/logsDataCleaner'; -import { IOutputService } from 'vs/workbench/contrib/output/common/output'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { supportsTelemetry } from 'vs/platform/telemetry/common/telemetryUtils'; import { IProductService } from 'vs/platform/product/common/productService'; -import { createCancelablePromise, timeout } from 'vs/base/common/async'; -import { canceled, getErrorMessage, isCancellationError } from 'vs/base/common/errors'; -import { CancellationToken } from 'vs/base/common/cancellation'; +import { URI } from 'vs/base/common/uri'; const workbenchActionsRegistry = Registry.as<IWorkbenchActionRegistry>(WorkbenchActionExtensions.WorkbenchActions); workbenchActionsRegistry.registerWorkbenchAction(SyncActionDescriptor.from(SetLogLevelAction), 'Developer: Set Log Level...', CATEGORIES.Developer.value); @@ -38,15 +31,9 @@ class LogOutputChannels extends Disposable implements IWorkbenchContribution { @IProductService private readonly productService: IProductService, @ILogService private readonly logService: ILogService, @IFileService private readonly fileService: IFileService, - @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); this.registerCommonContributions(); - if (isWeb) { - this.registerWebContributions(); - } else { - this.registerNativeContributions(); - } } private registerCommonContributions(): void { @@ -84,47 +71,9 @@ class LogOutputChannels extends Disposable implements IWorkbenchContribution { }); } - private registerWebContributions(): void { - this.instantiationService.createInstance(LogsDataCleaner); - - const workbenchActionsRegistry = Registry.as<IWorkbenchActionRegistry>(WorkbenchActionExtensions.WorkbenchActions); - workbenchActionsRegistry.registerWorkbenchAction(SyncActionDescriptor.from(OpenWindowSessionLogFileAction), 'Developer: Open Window Log File (Session)...', CATEGORIES.Developer.value); - } - - private registerNativeContributions(): void { - this.registerLogChannel(Constants.mainLogChannelId, nls.localize('mainLog', "Main"), URI.file(join(this.environmentService.logsPath, `main.log`))); - this.registerLogChannel(Constants.sharedLogChannelId, nls.localize('sharedLog', "Shared"), URI.file(join(this.environmentService.logsPath, `sharedprocess.log`))); - } - - private async registerLogChannel(id: string, label: string, file: URI): Promise<void> { - await whenProviderRegistered(file, this.fileService); - const outputChannelRegistry = Registry.as<IOutputChannelRegistry>(OutputExt.OutputChannels); - try { - const promise = createCancelablePromise(token => this.whenFileExists(file, 1, token)); - this._register(toDisposable(() => promise.cancel())); - await promise; - outputChannelRegistry.registerChannel({ id, label, file, log: true }); - } catch (error) { - if (!isCancellationError(error)) { - this.logService.error('Error while registering log channel', file.toString(), getErrorMessage(error)); - } - } - } - - private async whenFileExists(file: URI, trial: number, token: CancellationToken): Promise<void> { - const exists = await this.fileService.exists(file); - if (exists) { - return; - } - if (token.isCancellationRequested) { - throw canceled(); - } - if (trial > 10) { - throw new Error(`Timed out while waiting for file to be created`); - } - this.logService.debug(`[Registering Log Channel] File does not exist. Waiting for 1s to retry.`, file.toString()); - await timeout(1000, token); - await this.whenFileExists(file, trial + 1, token); + private registerLogChannel(id: string, label: string, file: URI): void { + const promise = registerLogChannel(id, label, file, this.fileService, this.logService); + this._register(toDisposable(() => promise.cancel())); } } diff --git a/src/vs/workbench/contrib/logs/electron-sandbox/logs.contribution.ts b/src/vs/workbench/contrib/logs/electron-sandbox/logs.contribution.ts index 34b8d2ff3fa..022c66402f2 100644 --- a/src/vs/workbench/contrib/logs/electron-sandbox/logs.contribution.ts +++ b/src/vs/workbench/contrib/logs/electron-sandbox/logs.contribution.ts @@ -3,10 +3,46 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as nls from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchActionRegistry, Extensions as WorkbenchActionExtensions, CATEGORIES } from 'vs/workbench/common/actions'; import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; import { OpenLogsFolderAction, OpenExtensionLogsFolderAction } from 'vs/workbench/contrib/logs/electron-sandbox/logsActions'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import * as Constants from 'vs/workbench/contrib/logs/common/logConstants'; +import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IFileService } from 'vs/platform/files/common/files'; +import { ILogService } from 'vs/platform/log/common/log'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { join } from 'vs/base/common/path'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { registerLogChannel } from 'vs/workbench/services/output/common/output'; + +class NativeLogOutputChannels extends Disposable implements IWorkbenchContribution { + + constructor( + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @ILogService private readonly logService: ILogService, + @IFileService private readonly fileService: IFileService, + ) { + super(); + this.registerNativeContributions(); + } + + private registerNativeContributions(): void { + this.registerLogChannel(Constants.mainLogChannelId, nls.localize('mainLog', "Main"), URI.file(join(this.environmentService.logsPath, `main.log`))); + this.registerLogChannel(Constants.sharedLogChannelId, nls.localize('sharedLog', "Shared"), URI.file(join(this.environmentService.logsPath, `sharedprocess.log`))); + } + + private registerLogChannel(id: string, label: string, file: URI): void { + const promise = registerLogChannel(id, label, file, this.fileService, this.logService); + this._register(toDisposable(() => promise.cancel())); + } + +} + +Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(NativeLogOutputChannels, LifecyclePhase.Restored); const workbenchActionsRegistry = Registry.as<IWorkbenchActionRegistry>(WorkbenchActionExtensions.WorkbenchActions); workbenchActionsRegistry.registerWorkbenchAction(SyncActionDescriptor.from(OpenLogsFolderAction), 'Developer: Open Logs Folder', CATEGORIES.Developer.value); diff --git a/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts b/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts index c47ff0b58ff..68185694831 100644 --- a/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts +++ b/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts @@ -121,7 +121,7 @@ code > div { } .vscode-high-contrast code > div { - background-color: rgb(0, 0, 0); + background-color: var(--vscode-textCodeBlock-background); } .vscode-high-contrast h1 { diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/statusBarProviders.ts b/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/statusBarProviders.ts index f1834e4ee96..750af12fdf4 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/statusBarProviders.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/statusBarProviders.ts @@ -5,16 +5,21 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { Disposable } from 'vs/base/common/lifecycle'; +import { ResourceMap } from 'vs/base/common/map'; import { URI } from 'vs/base/common/uri'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { localize } from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; -import { CHANGE_CELL_LANGUAGE } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CHANGE_CELL_LANGUAGE, DETECT_CELL_LANGUAGE } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService'; import { CellKind, CellStatusbarAlignment, INotebookCellStatusBarItem, INotebookCellStatusBarItemList, INotebookCellStatusBarItemProvider } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; +import { ILanguageDetectionService } from 'vs/workbench/services/languageDetection/common/languageDetectionWorkerService'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; class CellStatusBarLanguagePickerProvider implements INotebookCellStatusBarItemProvider { @@ -50,6 +55,80 @@ class CellStatusBarLanguagePickerProvider implements INotebookCellStatusBarItemP } } +class CellStatusBarLanguageDetectionProvider implements INotebookCellStatusBarItemProvider { + + readonly viewType = '*'; + + private cache = new ResourceMap<{ + lastUpdate: number; + lastCellLang: string; + lastGuess?: string; + }>(); + + constructor( + @INotebookService private readonly _notebookService: INotebookService, + @INotebookKernelService private readonly _notebookKernelService: INotebookKernelService, + @ILanguageService private readonly _languageService: ILanguageService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @ILanguageDetectionService private readonly _languageDetectionService: ILanguageDetectionService, + @IKeybindingService private readonly _keybindingService: IKeybindingService, + ) { } + + async provideCellStatusBarItems(uri: URI, index: number, token: CancellationToken): Promise<INotebookCellStatusBarItemList | undefined> { + const doc = this._notebookService.getNotebookTextModel(uri); + const cell = doc?.cells[index]; + if (!cell) { return; } + + const enablementConfig = this._configurationService.getValue('workbench.editor.languageDetectionHints'); + const enabled = enablementConfig === 'always' || enablementConfig === 'notebookEditors'; + if (!enabled) { + return; + } + + const currentLanguageId = cell.cellKind === CellKind.Markup ? + 'markdown' : + (this._languageService.getLanguageIdByLanguageName(cell.language) || cell.language); + + if (!this.cache.has(uri)) { + this.cache.set(uri, { lastCellLang: currentLanguageId, lastUpdate: 0 }); + } + + const cached = this.cache.get(uri)!; + if (cached.lastUpdate < Date.now() - 1000 || cached.lastCellLang !== currentLanguageId) { + cached.lastUpdate = Date.now(); + cached.lastCellLang = currentLanguageId; + + const kernel = this._notebookKernelService.getSelectedOrSuggestedKernel(doc); + + if (kernel) { + const availableLangs = []; + availableLangs.push(...kernel.supportedLanguages, 'markdown'); + cached.lastGuess = await this._languageDetectionService.detectLanguage(cell.uri, availableLangs); + } + } + + const items: INotebookCellStatusBarItem[] = []; + if (cached.lastGuess && currentLanguageId !== cached.lastGuess) { + const detectedName = this._languageService.getLanguageName(cached.lastGuess) || cached.lastGuess; + let tooltip = localize('notebook.cell.status.autoDetectLanguage', "Accept Detected Language: {0}", detectedName); + const keybinding = this._keybindingService.lookupKeybinding(DETECT_CELL_LANGUAGE); + const label = keybinding?.getLabel(); + if (label) { + tooltip += ` (${label})`; + } + items.push({ + text: '$(lightbulb-autofix)', + command: DETECT_CELL_LANGUAGE, + tooltip, + alignment: CellStatusbarAlignment.Right, + priority: -Number.MAX_SAFE_INTEGER + 1 + }); + } + + return { items }; + } +} + class BuiltinCellStatusBarProviders extends Disposable { constructor( @IInstantiationService instantiationService: IInstantiationService, @@ -58,6 +137,7 @@ class BuiltinCellStatusBarProviders extends Disposable { const builtinProviders = [ CellStatusBarLanguagePickerProvider, + CellStatusBarLanguageDetectionProvider, ]; builtinProviders.forEach(p => { this._register(notebookCellStatusBarService.registerCellStatusBarItemProvider(instantiationService.createInstance(p))); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard.ts b/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard.ts index 10037c96943..8fbc0999419 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard.ts @@ -27,7 +27,7 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation import { RedoCommand, UndoCommand } from 'vs/editor/browser/editorExtensions'; import { IWebview } from 'vs/workbench/contrib/webview/browser/webview'; import { CATEGORIES } from 'vs/workbench/common/actions'; -import { IOutputService } from 'vs/workbench/contrib/output/common/output'; +import { IOutputService } from 'vs/workbench/services/output/common/output'; import { rendererLogChannelId } from 'vs/workbench/contrib/logs/common/logConstants'; import { ILogService } from 'vs/platform/log/common/log'; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts b/src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts index cb7ab0f8586..920669194f6 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts @@ -28,7 +28,7 @@ import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/note import { configureKernelIcon, selectKernelIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { NotebookCellsChangeType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { INotebookKernel, INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { INotebookKernel, INotebookKernelService, NotebookKernelType } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; @@ -38,7 +38,7 @@ import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeat registerAction2(class extends Action2 { constructor() { super({ - id: '_notebook.selectKernel', + id: SELECT_KERNEL_ID, category: NOTEBOOK_ACTIONS_CATEGORY, title: { value: nls.localize('notebookActions.selectKernel', "Select Notebook Kernel"), original: 'Select Notebook Kernel' }, // precondition: NOTEBOOK_IS_ACTIVE_EDITOR, @@ -182,12 +182,7 @@ registerAction2(class extends Action2 { return res; } const quickPickItems: QuickPickInput<IQuickPickItem | KernelPick>[] = []; - if (!all.length) { - quickPickItems.push({ - id: 'install', - label: nls.localize('installKernels', "Install kernels from the marketplace"), - }); - } else { + if (all.length) { // Always display suggested kernels on the top. if (suggestions.length) { quickPickItems.push({ @@ -210,6 +205,14 @@ registerAction2(class extends Action2 { }); } + if (!all.find(item => item.type === NotebookKernelType.Resolved)) { + // there is no resolved kernel, show the install from marketplace + quickPickItems.push({ + id: 'install', + label: nls.localize('installKernels', "Install kernels from the marketplace"), + }); + } + const pick = await quickInputService.pick(quickPickItems, { placeHolder: selected ? nls.localize('prompt.placeholder.change', "Change kernel for '{0}'", labelService.getUriLabel(notebook.uri, { relative: true })) @@ -381,7 +384,7 @@ export class KernelStatus extends Disposable implements IWorkbenchContribution { tooltip: isSuggested ? nls.localize('tooltop', "{0} (suggestion)", tooltip) : tooltip, command: SELECT_KERNEL_ID, }, - '_notebook.selectKernel', + SELECT_KERNEL_ID, StatusbarAlignment.RIGHT, 10 )); @@ -399,7 +402,7 @@ export class KernelStatus extends Disposable implements IWorkbenchContribution { command: SELECT_KERNEL_ID, backgroundColor: { id: 'statusBarItem.prominentBackground' } }, - '_notebook.selectKernel', + SELECT_KERNEL_ID, StatusbarAlignment.RIGHT, 10 )); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/findModel.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/findModel.ts index f1b0150286c..2f5d35ba93f 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/findModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/findModel.ts @@ -88,7 +88,7 @@ export class FindModel extends Disposable { }; } - find(previous: boolean) { + find(option: { previous: boolean } | { index: number }) { if (!this.findMatches.length) { return; } @@ -96,14 +96,20 @@ export class FindModel extends Disposable { // let currCell; if (!this._findMatchesStarts) { this.set(this._findMatches, true); + if ('index' in option) { + this._currentMatch = option.index; + } } else { // const currIndex = this._findMatchesStarts!.getIndexOf(this._currentMatch); // currCell = this._findMatches[currIndex.index].cell; const totalVal = this._findMatchesStarts.getTotalSum(); - if (this._currentMatch === -1) { - this._currentMatch = previous ? totalVal - 1 : 0; + if ('index' in option) { + this._currentMatch = option.index; + } + else if (this._currentMatch === -1) { + this._currentMatch = option.previous ? totalVal - 1 : 0; } else { - const nextVal = (this._currentMatch + (previous ? -1 : 1) + totalVal) % totalVal; + const nextVal = (this._currentMatch + (option.previous ? -1 : 1) + totalVal) % totalVal; this._currentMatch = nextVal; } } @@ -157,8 +163,8 @@ export class FindModel extends Disposable { } async research() { - this._throttledDelayer.trigger(() => { - this._research(); + return this._throttledDelayer.trigger(() => { + return this._research(); }); } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFind.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFind.ts new file mode 100644 index 00000000000..f1ff2386aa7 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFind.ts @@ -0,0 +1,135 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./media/notebookFind'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { Schemas } from 'vs/base/common/network'; +import { isEqual } from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { ITextModel } from 'vs/editor/common/model'; +import { StartFindAction, StartFindReplaceAction } from 'vs/editor/contrib/find/browser/findController'; +import { localize } from 'vs/nls'; +import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { NotebookFindWidget } from 'vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget'; +import { getNotebookEditorFromEditorPane } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { registerNotebookContribution } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; +import { CellUri } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; + +registerNotebookContribution(NotebookFindWidget.id, NotebookFindWidget); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'notebook.hideFind', + title: { value: localize('notebookActions.hideFind', "Hide Find in Notebook"), original: 'Hide Find in Notebook' }, + keybinding: { + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED), + primary: KeyCode.Escape, + weight: KeybindingWeight.WorkbenchContrib + } + }); + } + + async run(accessor: ServicesAccessor): Promise<void> { + const editorService = accessor.get(IEditorService); + const editor = getNotebookEditorFromEditorPane(editorService.activeEditorPane); + + if (!editor) { + return; + } + + const controller = editor.getContribution<NotebookFindWidget>(NotebookFindWidget.id); + controller.hide(); + editor.focus(); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'notebook.find', + title: { value: localize('notebookActions.findInNotebook', "Find in Notebook"), original: 'Find in Notebook' }, + keybinding: { + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR, EditorContextKeys.focus.toNegated()), + primary: KeyCode.KeyF | KeyMod.CtrlCmd, + weight: KeybindingWeight.WorkbenchContrib + } + }); + } + + async run(accessor: ServicesAccessor): Promise<void> { + const editorService = accessor.get(IEditorService); + const editor = getNotebookEditorFromEditorPane(editorService.activeEditorPane); + + if (!editor) { + return; + } + + const controller = editor.getContribution<NotebookFindWidget>(NotebookFindWidget.id); + controller.show(); + } +}); + +function notebookContainsTextModel(uri: URI, textModel: ITextModel) { + if (textModel.uri.scheme === Schemas.vscodeNotebookCell) { + const cellUri = CellUri.parse(textModel.uri); + if (cellUri && isEqual(cellUri.notebook, uri)) { + return true; + } + } + + return false; +} + + +StartFindAction.addImplementation(100, (accessor: ServicesAccessor, codeEditor: ICodeEditor, args: any) => { + const editorService = accessor.get(IEditorService); + const editor = getNotebookEditorFromEditorPane(editorService.activeEditorPane); + + if (!editor) { + return false; + } + + if (!editor.hasEditorFocus() && !editor.hasWebviewFocus()) { + const codeEditorService = accessor.get(ICodeEditorService); + // check if the active pane contains the active text editor + const textEditor = codeEditorService.getFocusedCodeEditor() || codeEditorService.getActiveCodeEditor(); + if (editor.hasModel() && textEditor && textEditor.hasModel() && notebookContainsTextModel(editor.textModel.uri, textEditor.getModel())) { + // the active text editor is in notebook editor + } else { + return false; + } + } + + const controller = editor.getContribution<NotebookFindWidget>(NotebookFindWidget.id); + controller.show(); + return true; +}); + +StartFindReplaceAction.addImplementation(100, (accessor: ServicesAccessor, codeEditor: ICodeEditor, args: any) => { + const editorService = accessor.get(IEditorService); + const editor = getNotebookEditorFromEditorPane(editorService.activeEditorPane); + + if (!editor) { + return false; + } + + const controller = editor.getContribution<NotebookFindWidget>(NotebookFindWidget.id); + if (controller) { + controller.replace(); + return true; + } + + return false; +}); + diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts index 9e7e660b6b0..329490a8431 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts @@ -614,7 +614,7 @@ export abstract class SimpleFindReplaceWidget extends Widget { this._findInput.focus(); } - public show(initialInput?: string): void { + public show(initialInput?: string, options?: { focus?: boolean }): void { if (initialInput && !this._isVisible) { this._findInput.setValue(initialInput); } @@ -625,7 +625,9 @@ export abstract class SimpleFindReplaceWidget extends Widget { this._domNode.classList.add('visible', 'visible-transition'); this._domNode.setAttribute('aria-hidden', 'false'); - this.focus(); + if (options?.focus ?? true) { + this.focus(); + } }, 0); } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/findController.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget.ts index 662b1929a2e..c027d8137d4 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/findController.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget.ts @@ -3,46 +3,42 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./media/notebookFind'; +import * as DOM from 'vs/base/browser/dom'; +import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { alert as alertFn } from 'vs/base/browser/ui/aria/aria'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import * as strings from 'vs/base/common/strings'; -import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; -import { IContextKeyService, IContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; -import { INotebookEditor, CellEditState, INotebookEditorContribution, getNotebookEditorFromEditorPane } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { Range } from 'vs/editor/common/core/range'; +import { FindMatch } from 'vs/editor/common/model'; import { MATCHES_LIMIT } from 'vs/editor/contrib/find/browser/findModel'; -import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; -import * as DOM from 'vs/base/browser/dom'; -import { registerNotebookContribution } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; -import { registerAction2, Action2, IMenuService } from 'vs/platform/actions/common/actions'; -import { localize } from 'vs/nls'; -import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { FindReplaceState } from 'vs/editor/contrib/find/browser/findState'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { StartFindAction, StartFindReplaceAction } from 'vs/editor/contrib/find/browser/findController'; -import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { NLS_MATCHES_LOCATION, NLS_NO_RESULTS } from 'vs/editor/contrib/find/browser/findWidget'; +import { localize } from 'vs/nls'; +import { IMenuService } from 'vs/platform/actions/common/actions'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { NotebookFindFilters } from 'vs/workbench/contrib/notebook/browser/contrib/find/findFilters'; import { FindModel } from 'vs/workbench/contrib/notebook/browser/contrib/find/findModel'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { FindMatch, ITextModel } from 'vs/editor/common/model'; import { SimpleFindReplaceWidget } from 'vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget'; -import { NotebookFindFilters } from 'vs/workbench/contrib/notebook/browser/contrib/find/findFilters'; -import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { Schemas } from 'vs/base/common/network'; -import { CellUri } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { URI } from 'vs/base/common/uri'; -import { isEqual } from 'vs/base/common/resources'; +import { CellEditState, INotebookEditor, INotebookEditorContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; const FIND_HIDE_TRANSITION = 'find-hide-transition'; const FIND_SHOW_TRANSITION = 'find-show-transition'; let MAX_MATCHES_COUNT_WIDTH = 69; const PROGRESS_BAR_DELAY = 200; // show progress for at least 200ms +export interface IShowNotebookFindWidgetOptions { + isRegex?: boolean; + wholeWord?: boolean; + matchCase?: boolean; + matchIndex?: number; + focus?: boolean; +} + export class NotebookFindWidget extends SimpleFindReplaceWidget implements INotebookEditorContribution { static id: string = 'workbench.notebook.find'; protected _findWidgetFocused: IContextKey<boolean>; @@ -104,7 +100,7 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote private _onFindInputKeyDown(e: IKeyboardEvent): void { if (e.equals(KeyCode.Enter)) { - this._findModel.find(false); + this.find(false); e.preventDefault(); return; } else if (e.equals(KeyMod.Shift | KeyCode.Enter)) { @@ -125,8 +121,12 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote return false; } + private findIndex(index: number): void { + this._findModel.find({ index }); + } + protected find(previous: boolean): void { - this._findModel.find(previous); + this._findModel.find({ previous }); } protected replaceOne() { @@ -141,7 +141,7 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote this._findModel.ensureFindMatches(); if (this._findModel.currentMatch < 0) { - this._findModel.find(false); + this._findModel.find({ previous: false }); } const currentMatch = this._findModel.getCurrentMatch(); @@ -213,10 +213,18 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote protected onFindInputFocusTrackerFocus(): void { } protected onFindInputFocusTrackerBlur(): void { } - override show(initialInput?: string): void { - super.show(initialInput); + override async show(initialInput?: string, options?: IShowNotebookFindWidgetOptions): Promise<void> { + super.show(initialInput, options); this._state.change({ searchString: initialInput ?? '', isRevealed: true }, false); - this._findInput.select(); + + if (typeof options?.matchIndex === 'number') { + if (!this._findModel.findMatches.length) { + await this._findModel.research(); + } + this.findIndex(options.matchIndex); + } else { + this._findInput.select(); + } if (this._showTimeout === null) { if (this._hideTimeout !== null) { @@ -343,110 +351,3 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote super.dispose(); } } - -registerNotebookContribution(NotebookFindWidget.id, NotebookFindWidget); - -registerAction2(class extends Action2 { - constructor() { - super({ - id: 'notebook.hideFind', - title: { value: localize('notebookActions.hideFind', "Hide Find in Notebook"), original: 'Hide Find in Notebook' }, - keybinding: { - when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED), - primary: KeyCode.Escape, - weight: KeybindingWeight.WorkbenchContrib - } - }); - } - - async run(accessor: ServicesAccessor): Promise<void> { - const editorService = accessor.get(IEditorService); - const editor = getNotebookEditorFromEditorPane(editorService.activeEditorPane); - - if (!editor) { - return; - } - - const controller = editor.getContribution<NotebookFindWidget>(NotebookFindWidget.id); - controller.hide(); - editor.focus(); - } -}); - -registerAction2(class extends Action2 { - constructor() { - super({ - id: 'notebook.find', - title: { value: localize('notebookActions.findInNotebook', "Find in Notebook"), original: 'Find in Notebook' }, - keybinding: { - when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR, EditorContextKeys.focus.toNegated()), - primary: KeyCode.KeyF | KeyMod.CtrlCmd, - weight: KeybindingWeight.WorkbenchContrib - } - }); - } - - async run(accessor: ServicesAccessor): Promise<void> { - const editorService = accessor.get(IEditorService); - const editor = getNotebookEditorFromEditorPane(editorService.activeEditorPane); - - if (!editor) { - return; - } - - const controller = editor.getContribution<NotebookFindWidget>(NotebookFindWidget.id); - controller.show(); - } -}); - -function notebookContainsTextModel(uri: URI, textModel: ITextModel) { - if (textModel.uri.scheme === Schemas.vscodeNotebookCell) { - const cellUri = CellUri.parse(textModel.uri); - if (cellUri && isEqual(cellUri.notebook, uri)) { - return true; - } - } - - return false; -} - -StartFindAction.addImplementation(100, (accessor: ServicesAccessor, codeEditor: ICodeEditor, args: any) => { - const editorService = accessor.get(IEditorService); - const editor = getNotebookEditorFromEditorPane(editorService.activeEditorPane); - - if (!editor) { - return false; - } - - if (!editor.hasEditorFocus() && !editor.hasWebviewFocus()) { - const codeEditorService = accessor.get(ICodeEditorService); - // check if the active pane contains the active text editor - const textEditor = codeEditorService.getFocusedCodeEditor() || codeEditorService.getActiveCodeEditor(); - if (editor.hasModel() && textEditor && textEditor.hasModel() && notebookContainsTextModel(editor.textModel.uri, textEditor.getModel())) { - // the active text editor is in notebook editor - } else { - return false; - } - } - - const controller = editor.getContribution<NotebookFindWidget>(NotebookFindWidget.id); - controller.show(); - return true; -}); - -StartFindReplaceAction.addImplementation(100, (accessor: ServicesAccessor, codeEditor: ICodeEditor, args: any) => { - const editorService = accessor.get(IEditorService); - const editor = getNotebookEditorFromEditorPane(editorService.activeEditorPane); - - if (!editor) { - return false; - } - - const controller = editor.getContribution<NotebookFindWidget>(NotebookFindWidget.id); - if (controller) { - controller.replace(); - return true; - } - - return false; -}); diff --git a/src/vs/workbench/contrib/notebook/browser/controller/apiActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/apiActions.ts index d0c6379a54b..68d9d0f8dcc 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/apiActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/apiActions.ts @@ -7,7 +7,7 @@ import * as glob from 'vs/base/common/glob'; import { URI, UriComponents } from 'vs/base/common/uri'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { isDocumentExcludePattern, TransientCellMetadata, TransientDocumentMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { INotebookKernelService, IResolvedNotebookKernel, NotebookKernelType } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; CommandsRegistry.registerCommand('_resolveNotebookContentProvider', (accessor): { @@ -66,13 +66,13 @@ CommandsRegistry.registerCommand('_resolveNotebookKernels', async (accessor, arg const uri = URI.revive(args.uri as UriComponents); const kernels = notebookKernelService.getMatchingKernel({ uri, viewType: args.viewType }); - return kernels.all.map(provider => ({ + return kernels.all.filter(kernel => kernel.type === NotebookKernelType.Resolved).map((provider) => ({ id: provider.id, label: provider.label, kind: provider.kind, description: provider.description, detail: provider.detail, isPreferred: false, // todo@jrieken,@rebornix - preloads: provider.preloadUris, + preloads: (provider as IResolvedNotebookKernel).preloadUris, })); }); diff --git a/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts index 588675f6403..8d253828d2e 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts @@ -21,12 +21,14 @@ import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/ import { changeCellToKind, runDeleteAction } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations'; import { CellToolbarOrder, CELL_TITLE_CELL_GROUP_ID, CELL_TITLE_OUTPUT_GROUP_ID, executeNotebookCondition, INotebookActionContext, INotebookCellActionContext, NotebookAction, NotebookCellAction, NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; import { NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_TYPE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_HAS_OUTPUTS, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_USE_CONSOLIDATED_OUTPUT_BUTTON } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; -import { CellEditState, CHANGE_CELL_LANGUAGE, QUIT_EDIT_CELL_COMMAND_ID } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellEditState, CHANGE_CELL_LANGUAGE, DETECT_CELL_LANGUAGE, QUIT_EDIT_CELL_COMMAND_ID } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import * as icons from 'vs/workbench/contrib/notebook/browser/notebookIcons'; import { CellEditType, CellKind, ICellEditOperation, NotebookCellExecutionState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { ILanguageDetectionService } from 'vs/workbench/services/languageDetection/common/languageDetectionWorkerService'; import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; const CLEAR_ALL_CELLS_OUTPUTS_COMMAND_ID = 'notebook.clearAllCellsOutputs'; const EDIT_CELL_COMMAND_ID = 'notebook.cell.edit'; @@ -437,23 +439,7 @@ registerAction2(class ChangeCellLanguageAction extends NotebookCellAction<ICellR } private async setLanguage(context: IChangeCellContext, languageId: string) { - if (languageId === 'markdown' && context.cell?.language !== 'markdown') { - const idx = context.notebookEditor.getCellIndex(context.cell); - await changeCellToKind(CellKind.Markup, { cell: context.cell, notebookEditor: context.notebookEditor, ui: true }, 'markdown', Mimes.markdown); - const newCell = context.notebookEditor.cellAt(idx); - - if (newCell) { - context.notebookEditor.focusNotebookCell(newCell, 'editor'); - } - } else if (languageId !== 'markdown' && context.cell?.cellKind === CellKind.Markup) { - await changeCellToKind(CellKind.Code, { cell: context.cell, notebookEditor: context.notebookEditor, ui: true }, languageId); - } else { - const index = context.notebookEditor.textModel.cells.indexOf(context.cell.model); - context.notebookEditor.textModel.applyEdits( - [{ editType: CellEditType.CellLanguage, index, language: languageId }], - true, undefined, () => undefined, undefined, true - ); - } + await setCellToLanguage(languageId, context); } /** @@ -478,3 +464,50 @@ registerAction2(class ChangeCellLanguageAction extends NotebookCellAction<ICellR return fakeResource; } }); + +registerAction2(class DetectCellLanguageAction extends NotebookCellAction { + constructor() { + super({ + id: DETECT_CELL_LANGUAGE, + title: localize('detectLanguage', 'Accept Detected Language for Cell'), + f1: true, + precondition: ContextKeyExpr.and(NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE), + keybinding: { primary: KeyCode.KeyD | KeyMod.Alt | KeyMod.Shift, weight: KeybindingWeight.WorkbenchContrib } + }); + } + + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise<void> { + const languageDetectionService = accessor.get(ILanguageDetectionService); + const notificationService = accessor.get(INotificationService); + const kernelService = accessor.get(INotebookKernelService); + const kernel = kernelService.getSelectedOrSuggestedKernel(context.notebookEditor.textModel); + const providerLanguages = [...kernel?.supportedLanguages ?? []]; + providerLanguages.push('markdown'); + const detection = await languageDetectionService.detectLanguage(context.cell.uri, providerLanguages); + if (detection) { + setCellToLanguage(detection, context); + } else { + notificationService.warn(localize('noDetection', "Unable to detect cell language")); + } + } +}); + +async function setCellToLanguage(languageId: string, context: IChangeCellContext) { + if (languageId === 'markdown' && context.cell?.language !== 'markdown') { + const idx = context.notebookEditor.getCellIndex(context.cell); + await changeCellToKind(CellKind.Markup, { cell: context.cell, notebookEditor: context.notebookEditor, ui: true }, 'markdown', Mimes.markdown); + const newCell = context.notebookEditor.cellAt(idx); + + if (newCell) { + context.notebookEditor.focusNotebookCell(newCell, 'editor'); + } + } else if (languageId !== 'markdown' && context.cell?.cellKind === CellKind.Markup) { + await changeCellToKind(CellKind.Code, { cell: context.cell, notebookEditor: context.notebookEditor, ui: true }, languageId); + } else { + const index = context.notebookEditor.textModel.cells.indexOf(context.cell.model); + context.notebookEditor.textModel.applyEdits( + [{ editType: CellEditType.CellLanguage, index, language: languageId }], + true, undefined, () => undefined, undefined, true + ); + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/controller/layoutActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/layoutActions.ts index 27ce6ca5434..4f6a3e489c5 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/layoutActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/layoutActions.ts @@ -173,15 +173,6 @@ registerAction2(class ToggleBreadcrumbFromEditorTitle extends Action2 { } }); -MenuRegistry.appendMenuItem(MenuId.NotebookToolbar, { - command: { - id: 'breadcrumbs.toggle', - title: { value: localize('cmd.toggle', "Toggle Breadcrumbs"), original: 'Toggle Breadcrumbs' }, - }, - group: 'notebookLayout', - order: 2 -}); - registerAction2(class SaveMimeTypeDisplayOrder extends Action2 { constructor() { super({ diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/src/vs/workbench/contrib/notebook/browser/media/notebook.css index a660892ff1f..e4220b57b14 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -429,7 +429,7 @@ outline: none !important; } -.monaco-workbench .notebookOverlay.notebook-editor-editable > .cell-list-container > .monaco-list > .monaco-scrollable-element > .scrollbar.visible { +.monaco-workbench .notebookOverlay.notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .scrollbar.visible { z-index: var(--z-index-notebook-scrollbar); cursor: default; } diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 2b793c40961..466fd1bf565 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -70,7 +70,7 @@ import 'vs/workbench/contrib/notebook/browser/controller/foldingController'; // Editor Contribution import 'vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard'; -import 'vs/workbench/contrib/notebook/browser/contrib/find/findController'; +import 'vs/workbench/contrib/notebook/browser/contrib/find/notebookFind'; import 'vs/workbench/contrib/notebook/browser/contrib/format/formatting'; import 'vs/workbench/contrib/notebook/browser/contrib/gettingStarted/notebookGettingStarted'; import 'vs/workbench/contrib/notebook/browser/contrib/layout/layoutActions'; @@ -892,5 +892,22 @@ configurationRegistry.registerConfiguration({ enum: ['always', 'never', 'fromEditor'], default: 'fromEditor' }, + [NotebookSetting.outputLineHeight]: { + markdownDescription: nls.localize('notebook.outputLineHeight', "Line height of the output text for notebook cells.\n - Values between 0 and 8 will be used as a multiplier with the font size.\n - Values greater than or equal to 8 will be used as effective values."), + type: 'number', + default: 22, + tags: ['notebookLayout'] + }, + [NotebookSetting.outputFontSize]: { + markdownDescription: nls.localize('notebook.outputFontSize', "Font size for the output text for notebook cells. When set to 0 `#editor.fontSize#` is used."), + type: 'number', + default: 0, + tags: ['notebookLayout'] + }, + [NotebookSetting.outputFontFamily]: { + markdownDescription: nls.localize('notebook.outputFontFamily', "The font family for the output text for notebook cells. When set to empty, the `#editor.fontFamily#` is used."), + type: 'string', + tags: ['notebookLayout'] + }, } }); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index 7153bcfe26c..77a50b8c655 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -11,7 +11,7 @@ import { IEditorContributionDescription } from 'vs/editor/browser/editorExtensio import * as editorCommon from 'vs/editor/common/editorCommon'; import { FontInfo } from 'vs/editor/common/config/fontInfo'; import { IPosition } from 'vs/editor/common/core/position'; -import { Range } from 'vs/editor/common/core/range'; +import { IRange, Range } from 'vs/editor/common/core/range'; import { FindMatch, IModelDeltaDecoration, IReadonlyTextBuffer, ITextModel, TrackedRangeStickiness } from 'vs/editor/common/model'; import { MenuId } from 'vs/platform/actions/common/actions'; import { ITextEditorOptions, ITextResourceEditorInput } from 'vs/platform/editor/common/editor'; @@ -31,6 +31,7 @@ import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; //#region Shared commands export const EXPAND_CELL_INPUT_COMMAND_ID = 'notebook.cell.expandCellInput'; export const EXECUTE_CELL_COMMAND_ID = 'notebook.cell.execute'; +export const DETECT_CELL_LANGUAGE = 'notebook.cell.detectLanguage'; export const CHANGE_CELL_LANGUAGE = 'notebook.cell.changeLanguage'; export const QUIT_EDIT_CELL_COMMAND_ID = 'notebook.cell.quitEdit'; export const EXPAND_CELL_OUTPUT_COMMAND_ID = 'notebook.cell.expandCellOutput'; @@ -280,6 +281,7 @@ export interface INotebookEditorOptions extends ITextEditorOptions { readonly cellSelections?: ICellRange[]; readonly isReadOnly?: boolean; readonly viewState?: INotebookEditorViewState; + readonly indexedCellOptions?: { index: number; selection?: IRange }; } export type INotebookEditorContributionCtor = IConstructorSignature<INotebookEditorContribution, [INotebookEditor]>; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index 623686523fa..58edfc13e6b 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -7,7 +7,7 @@ import * as DOM from 'vs/base/browser/dom'; import { IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { IAction, toAction } from 'vs/base/common/actions'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IErrorWithActions } from 'vs/base/common/errorMessage'; +import { createErrorWithActions } from 'vs/base/common/errorMessage'; import { Emitter, Event } from 'vs/base/common/event'; import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import { extname, isEqual } from 'vs/base/common/resources'; @@ -289,8 +289,7 @@ export class NotebookEditor extends EditorPane implements IEditorPaneWithSelecti } } } catch (e) { - const error: Error & IErrorWithActions = e instanceof Error ? e : new Error(e.message); - error.actions = [ + const error = createErrorWithActions(e instanceof Error ? e : new Error(e.message), [ toAction({ id: 'workbench.notebook.action.openInTextEditor', label: localize('notebookOpenInTextEditor', "Open in Text Editor"), run: async () => { const activeEditorPane = this._editorService.activeEditorPane; @@ -317,7 +316,7 @@ export class NotebookEditor extends EditorPane implements IEditorPaneWithSelecti return; } }) - ]; + ]); throw error; } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index aa1bcc7853f..20ea78a5058 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -101,7 +101,6 @@ export class BaseCellEditorOptions extends Disposable implements IBaseCellEditor }, renderLineHighlightOnlyWhenFocus: true, overviewRulerLanes: 0, - selectOnLineNumbers: false, lineNumbers: 'off', lineDecorationsWidth: 0, folding: true, @@ -397,7 +396,11 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this._updateForNotebookConfiguration(); } - if (e.compactView || e.focusIndicator || e.insertToolbarPosition || e.cellToolbarLocation || e.dragAndDropEnabled || e.fontSize || e.markupFontSize || e.insertToolbarAlignment) { + if (e.fontFamily) { + this._generateFontInfo(); + } + + if (e.compactView || e.focusIndicator || e.insertToolbarPosition || e.cellToolbarLocation || e.dragAndDropEnabled || e.fontSize || e.outputFontSize || e.markupFontSize || e.fontFamily || e.outputFontFamily || e.insertToolbarAlignment || e.outputLineHeight) { this._styleElement?.remove(); this._createLayoutStyles(); this._webview?.updateOptions({ @@ -1216,8 +1219,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this.viewModel.updateOptions({ isReadOnly: this._readOnly }); // reveal cell if editor options tell to do so - if (options?.cellOptions) { - const cellOptions = options.cellOptions; + const cellOptions = options?.cellOptions ?? this._parseIndexedCellOptions(options); + if (cellOptions) { const cell = this.viewModel.viewCells.find(cell => cell.uri.toString() === cellOptions.resource.toString()); if (cell) { this.focusElement(cell); @@ -1270,6 +1273,24 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this._onDidChangeOptions.fire(); } + private _parseIndexedCellOptions(options: INotebookEditorOptions | undefined) { + if (options?.indexedCellOptions) { + // convert index based selections + const cell = this.cellAt(options.indexedCellOptions.index); + if (cell) { + return { + resource: cell.uri, + options: { + selection: options.indexedCellOptions.selection, + preserveFocus: false + } + }; + } + } + + return undefined; + } + private _detachModel() { this._localStore.clear(); dispose(this._localCellStateListeners); @@ -1802,6 +1823,11 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD const focusRange = this.viewModel.getFocus(); const element = this.viewModel.cellAt(focusRange.start); + // The notebook editor doesn't have focus yet + if (!this.hasEditorFocus()) { + this.focusContainer(); + } + if (element && element.focusMode === CellFocusMode.Editor) { element.updateEditState(CellEditState.Editing, 'editorWidget.focus'); element.focusMode = CellFocusMode.Editor; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookExecutionServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookExecutionServiceImpl.ts index 000c7b3b72c..2d4fe4cebe0 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookExecutionServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookExecutionServiceImpl.ts @@ -4,6 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { IDisposable } from 'vs/base/common/lifecycle'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { ILogService } from 'vs/platform/log/common/log'; import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; @@ -12,10 +14,11 @@ import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/mode import { CellKind, INotebookTextModel, NotebookCellExecutionState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookExecutionService } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; -import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { INotebookKernelService, NotebookKernelType } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; -export class NotebookExecutionService implements INotebookExecutionService { +export class NotebookExecutionService implements INotebookExecutionService, IDisposable { declare _serviceBrand: undefined; + private _activeProxyKernelExecutionToken: CancellationTokenSource | undefined; constructor( @ICommandService private readonly _commandService: ICommandService, @@ -45,6 +48,29 @@ export class NotebookExecutionService implements INotebookExecutionService { return; } + if (kernel.type === NotebookKernelType.Proxy) { + this._activeProxyKernelExecutionToken?.dispose(true); + const tokenSource = this._activeProxyKernelExecutionToken = new CancellationTokenSource(); + const resolved = await kernel.resolveKernel(notebook.uri); + const kernels = this._notebookKernelService.getMatchingKernel(notebook); + const newlyMatchedKernel = kernels.all.find(k => k.id === resolved); + + if (!newlyMatchedKernel) { + return; + } + + kernel = newlyMatchedKernel; + if (tokenSource.token.isCancellationRequested) { + // execution was cancelled but we still need to update the active kernel + this._notebookKernelService.selectKernelForNotebook(kernel, notebook); + return; + } + } + + if (kernel.type === NotebookKernelType.Proxy) { + return; + } + const executeCells: NotebookCellTextModel[] = []; for (const cell of cellsArr) { const cellExe = this._notebookExecutionStateService.getCellExecution(cell.uri); @@ -75,11 +101,20 @@ export class NotebookExecutionService implements INotebookExecutionService { this._logService.debug(`NotebookExecutionService#cancelNotebookCellHandles ${JSON.stringify(cellsArr)}`); const kernel = this._notebookKernelService.getSelectedOrSuggestedKernel(notebook); if (kernel) { - await kernel.cancelNotebookCellExecution(notebook.uri, cellsArr); + if (kernel.type === NotebookKernelType.Proxy) { + this._activeProxyKernelExecutionToken?.dispose(true); + } else { + await kernel.cancelNotebookCellExecution(notebook.uri, cellsArr); + } + } } async cancelNotebookCells(notebook: INotebookTextModel, cells: Iterable<NotebookCellTextModel>): Promise<void> { this.cancelNotebookCellHandles(notebook, Array.from(cells, cell => cell.handle)); } + + dispose() { + this._activeProxyKernelExecutionToken?.dispose(true); + } } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookKernelServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookKernelServiceImpl.ts index 1f2e2e72032..3a14f27d0ff 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookKernelServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookKernelServiceImpl.ts @@ -196,7 +196,16 @@ export class NotebookKernelService extends Disposable implements INotebookKernel getSelectedOrSuggestedKernel(notebook: INotebookTextModel): INotebookKernel | undefined { const info = this.getMatchingKernel(notebook); - return info.selected ?? (info.all.length === 1 ? info.all[0] : undefined); + if (info.selected) { + return info.selected; + } + + const preferred = info.all.filter(kernel => this._kernels.get(kernel.id)?.notebookPriorities.get(notebook.uri) === 2 /* vscode.NotebookControllerPriority.Preferred */); + if (preferred.length === 1) { + return preferred[0]; + } + + return info.all.length === 1 ? info.all[0] : undefined; } // default kernel for notebookType diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellContextKeys.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellContextKeys.ts index e1e2a982be5..364939e3456 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellContextKeys.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellContextKeys.ts @@ -5,15 +5,15 @@ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { CellEditState, CellFocusMode, ICellViewModel, INotebookEditorDelegate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { NotebookCellExecutionStateContext, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_EDITOR_FOCUSED, NOTEBOOK_CELL_EXECUTING, NOTEBOOK_CELL_EXECUTION_STATE, NOTEBOOK_CELL_FOCUSED, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_LINE_NUMBERS, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_TYPE } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import { CellViewModelStateChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; +import { CellPart } from 'vs/workbench/contrib/notebook/browser/view/cellPart'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel'; import { NotebookCellExecutionState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NotebookCellExecutionStateContext, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_EDITOR_FOCUSED, NOTEBOOK_CELL_EXECUTING, NOTEBOOK_CELL_EXECUTION_STATE, NOTEBOOK_CELL_FOCUSED, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_LINE_NUMBERS, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_RESOURCE, NOTEBOOK_CELL_TYPE } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; -import { CellViewModelStateChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; -import { CellPart } from 'vs/workbench/contrib/notebook/browser/view/cellPart'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; export class CellContextKeyPart extends CellPart { private cellContextKeyManager: CellContextKeyManager; @@ -44,6 +44,7 @@ export class CellContextKeyManager extends Disposable { private cellContentCollapsed!: IContextKey<boolean>; private cellOutputCollapsed!: IContextKey<boolean>; private cellLineNumbers!: IContextKey<'on' | 'off' | 'inherit'>; + private cellResource!: IContextKey<string>; private markdownEditMode!: IContextKey<boolean>; @@ -69,6 +70,7 @@ export class CellContextKeyManager extends Disposable { this.cellContentCollapsed = NOTEBOOK_CELL_INPUT_COLLAPSED.bindTo(this._contextKeyService); this.cellOutputCollapsed = NOTEBOOK_CELL_OUTPUT_COLLAPSED.bindTo(this._contextKeyService); this.cellLineNumbers = NOTEBOOK_CELL_LINE_NUMBERS.bindTo(this._contextKeyService); + this.cellResource = NOTEBOOK_CELL_RESOURCE.bindTo(this._contextKeyService); if (element) { this.updateForElement(element); @@ -112,6 +114,7 @@ export class CellContextKeyManager extends Disposable { this.updateForOutputs(); this.cellLineNumbers.set(this.element!.lineNumbers); + this.cellResource.set(this.element!.uri.toString()); }); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDnd.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDnd.ts index 8c0b8a99748..86e0a69074a 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDnd.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDnd.ts @@ -78,7 +78,7 @@ export class CellDragAndDropController extends Disposable { this._register(DOM.addDisposableListener(document.body, DOM.EventType.DRAG_START, this.onGlobalDragStart.bind(this), true)); this._register(DOM.addDisposableListener(document.body, DOM.EventType.DRAG_END, this.onGlobalDragEnd.bind(this), true)); - const addCellDragListener = (eventType: string, handler: (e: CellDragEvent) => void) => { + const addCellDragListener = (eventType: string, handler: (e: CellDragEvent) => void, useCapture = false) => { this._register(DOM.addDisposableListener( notebookEditor.getDomNode(), eventType, @@ -87,14 +87,21 @@ export class CellDragAndDropController extends Disposable { if (cellDragEvent) { handler(cellDragEvent); } - })); + }, useCapture)); }; addCellDragListener(DOM.EventType.DRAG_OVER, event => { + if (!this.currentDraggedCell) { + return; + } event.browserEvent.preventDefault(); + event.browserEvent.stopImmediatePropagation(); this.onCellDragover(event); - }); + }, true); addCellDragListener(DOM.EventType.DROP, event => { + if (!this.currentDraggedCell) { + return; + } event.browserEvent.preventDefault(); this.onCellDrop(event); }); diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDragRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDragRenderer.ts index 46388b581b4..43c5852753e 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDragRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDragRenderer.ts @@ -65,7 +65,7 @@ class EditorTextRenderer { let result = ''; for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) { - const lineTokens = model.getLineTokens(lineNumber); + const lineTokens = model.tokenization.getLineTokens(lineNumber); const lineContent = lineTokens.getLineContent(); const startOffset = (lineNumber === startLineNumber ? startColumn - 1 : 0); const endOffset = (lineNumber === endLineNumber ? endColumn - 1 : lineContent.length); diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellExecution.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellExecution.ts index d4745c02a43..469e7cbef3d 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellExecution.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellExecution.ts @@ -9,6 +9,7 @@ import { ICellViewModel, INotebookEditorDelegate } from 'vs/workbench/contrib/no import { CellViewModelStateChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; import { CellPart } from 'vs/workbench/contrib/notebook/browser/view/cellPart'; import { NotebookCellInternalMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NotebookKernelType } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; export class CellExecutionPart extends CellPart { private kernelDisposables = this._register(new DisposableStore()); @@ -41,7 +42,7 @@ export class CellExecutionPart extends CellPart { } private updateExecutionOrder(internalMetadata: NotebookCellInternalMetadata): void { - if (this._notebookEditor.activeKernel?.implementsExecutionOrder) { + if (this._notebookEditor.activeKernel?.type === NotebookKernelType.Resolved && this._notebookEditor.activeKernel?.implementsExecutionOrder) { const executionOrderLabel = typeof internalMetadata.executionOrder === 'number' ? `[${internalMetadata.executionOrder}]` : '[ ]'; @@ -62,7 +63,8 @@ export class CellExecutionPart extends CellPart { DOM.hide(this._executionOrderLabel); } else { DOM.show(this._executionOrderLabel); - this._executionOrderLabel.style.top = `${element.layoutInfo.editorHeight}px`; + const top = element.layoutInfo.editorHeight - 22 + element.layoutInfo.statusBarHeight; + this._executionOrderLabel.style.top = `${top}px`; } } } diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts index 3f2df96e305..6cd5d2c242d 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts @@ -316,8 +316,17 @@ export class CodeCell extends Disposable { })); } + private shouldUpdateDOMFocus() { + // The DOM focus needs to be adjusted: + // when a cell editor should be focused + // the document active element is inside the notebook editor or the document body (cell editor being disposed previously) + return this.notebookEditor.getActiveCell() === this.viewCell + && this.viewCell.focusMode === CellFocusMode.Editor + && (this.notebookEditor.hasEditorFocus() || document.activeElement === document.body); + } + private updateEditorForFocusModeChange() { - if (this.viewCell.focusMode === CellFocusMode.Editor && this.notebookEditor.getActiveCell() === this.viewCell) { + if (this.shouldUpdateDOMFocus()) { this.templateData.editor?.focus(); } @@ -479,7 +488,7 @@ export class CodeCell extends Disposable { this._isDisposed = true; // move focus back to the cell list otherwise the focus goes to body - if (this.notebookEditor.getActiveCell() === this.viewCell && this.viewCell.focusMode === CellFocusMode.Editor) { + if (this.shouldUpdateDOMFocus()) { this.notebookEditor.focusContainer(); } 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 bf119feb15c..82e710ed494 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/markdownCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/markdownCell.ts @@ -221,7 +221,7 @@ export class StatefulMarkdownCell extends Disposable { override dispose() { // move focus back to the cell list otherwise the focus goes to body - if (this.notebookEditor.getActiveCell() === this.viewCell && this.viewCell.focusMode === CellFocusMode.Editor) { + if (this.notebookEditor.getActiveCell() === this.viewCell && this.viewCell.focusMode === CellFocusMode.Editor && (this.notebookEditor.hasEditorFocus() || document.activeElement === document.body)) { this.notebookEditor.focusContainer(); } @@ -327,6 +327,7 @@ export class StatefulMarkdownCell extends Disposable { width: width, height: editorHeight }, + enableDropIntoEditor: true, // overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode() }, { contributions: this.notebookEditor.creationOptions.cellEditorContributions diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index 7bda661ed4e..a7145c9ffc9 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -37,10 +37,11 @@ import { preloadsScriptStr, RendererMetadata } from 'vs/workbench/contrib/notebo import { transformWebviewThemeVars } from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewThemeMapping'; import { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel'; import { CellUri, INotebookRendererInfo, NotebookSetting, RendererMessagingSpec } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { INotebookKernel } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { INotebookKernel, IResolvedNotebookKernel, NotebookKernelType } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { IScopedRendererMessaging } from 'vs/workbench/contrib/notebook/common/notebookRendererMessagingService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { IWebviewElement, IWebviewService, WebviewContentPurpose } from 'vs/workbench/contrib/webview/browser/webview'; +import { WebviewWindowDragMonitor } from 'vs/workbench/contrib/webview/browser/webviewWindowDragMonitor'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { FromWebviewMessage, IAckOutputHeight, IClickedDataUrlMessage, ICodeBlockHighlightRequest, IContentWidgetTopRequest, IControllerPreload, ICreationContent, ICreationRequestMessage, IFindMatch, IMarkupCellInitialization, ToWebviewMessage } from './webviewMessages'; @@ -88,8 +89,11 @@ interface BacklayerWebviewOptions { readonly runGutter: number; readonly dragAndDropEnabled: boolean; readonly fontSize: number; + readonly outputFontSize: number; readonly fontFamily: string; + readonly outputFontFamily: string; readonly markupFontSize: number; + readonly outputLineHeight: number; } export class BackLayerWebView<T extends ICommonCellInfo> extends Disposable { @@ -204,8 +208,9 @@ export class BackLayerWebView<T extends ICommonCellInfo> extends Disposable { 'notebook-output-node-left-padding': `${this.options.outputNodeLeftPadding}px`, 'notebook-markdown-min-height': `${this.options.previewNodePadding * 2}px`, 'notebook-markup-font-size': typeof this.options.markupFontSize === 'number' && this.options.markupFontSize > 0 ? `${this.options.markupFontSize}px` : `calc(${this.options.fontSize}px * 1.2)`, - 'notebook-cell-output-font-size': `${this.options.fontSize}px`, - 'notebook-cell-output-font-family': this.options.fontFamily, + 'notebook-cell-output-font-size': `${this.options.outputFontSize || this.options.fontSize}px`, + 'notebook-cell-output-line-height': `${this.options.outputLineHeight}px`, + 'notebook-cell-output-font-family': this.options.outputFontFamily || this.options.fontFamily, 'notebook-cell-markup-empty-content': nls.localize('notebook.emptyMarkdownPlaceholder', "Empty markdown cell, double click or press enter to edit."), 'notebook-cell-renderer-not-found-error': nls.localize({ key: 'notebook.error.rendererNotFound', @@ -523,6 +528,8 @@ var requirejs = (function() { this.webview.mountTo(this.element); this._register(this.webview); + this._register(new WebviewWindowDragMonitor(() => this.webview)); + this._register(this.webview.onDidClickLink(link => { if (this._disposed) { return; @@ -897,7 +904,7 @@ var requirejs = (function() { } this._preloadsCache.clear(); - if (this._currentKernel) { + if (this._currentKernel?.type === NotebookKernelType.Resolved) { this._updatePreloadsFromKernel(this._currentKernel); } @@ -1394,14 +1401,14 @@ var requirejs = (function() { const previousKernel = this._currentKernel; this._currentKernel = kernel; - if (previousKernel && previousKernel.preloadUris.length > 0) { + if (previousKernel?.type === NotebookKernelType.Resolved && previousKernel.preloadUris.length > 0) { this.webview?.reload(); // preloads will be restored after reload - } else if (kernel) { + } else if (kernel?.type === NotebookKernelType.Resolved) { this._updatePreloadsFromKernel(kernel); } } - private _updatePreloadsFromKernel(kernel: INotebookKernel) { + private _updatePreloadsFromKernel(kernel: IResolvedNotebookKernel) { const resources: IControllerPreload[] = []; for (const preload of kernel.preloadUris) { const uri = this.environmentService.isExtensionDevelopment && (preload.scheme === 'http' || preload.scheme === 'https') @@ -1427,7 +1434,7 @@ var requirejs = (function() { const mixedResourceRoots = [ ...(this.localResourceRootsCache || []), - ...(this._currentKernel ? [this._currentKernel.localResourceRoot] : []), + ...(this._currentKernel?.type === NotebookKernelType.Resolved ? [this._currentKernel.localResourceRoot] : []), ]; this.webview.localResourcesRoot = mixedResourceRoots; 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 02beef093cf..2bcf55c8531 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -168,7 +168,7 @@ export class MarkupCellRenderer extends AbstractCellRenderer implements IListRen templateDisposables.add(scopedInstaService.createInstance(BetweenCellToolbar, this.notebookEditor, titleToolbarContainer, bottomCellContainer)), templateDisposables.add(scopedInstaService.createInstance(CellEditorStatusBar, this.notebookEditor, container, editorPart, undefined)), templateDisposables.add(new CellFocusIndicator(this.notebookEditor, titleToolbar, focusIndicatorTop, focusIndicatorLeft, focusIndicatorRight, focusIndicatorBottom)), - templateDisposables.add(scopedInstaService.createInstance(FoldedCellHint, this.notebookEditor, DOM.append(container, $('.notebook-folded-hint')))), + templateDisposables.add(new FoldedCellHint(this.notebookEditor, DOM.append(container, $('.notebook-folded-hint')))), templateDisposables.add(new CellDecorations(rootContainer, decorationContainer)), templateDisposables.add(scopedInstaService.createInstance(CellComments, this.notebookEditor, cellCommentPartContainer)), templateDisposables.add(new CollapsedCellInput(this.notebookEditor, cellInputCollapsedContainer)), @@ -274,6 +274,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende width: 0, height: 0 }, + enableDropIntoEditor: true, }, { contributions: this.notebookEditor.creationOptions.cellEditorContributions }); diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts index 8b7c306af28..11b51e17e2f 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts @@ -156,8 +156,6 @@ export abstract class BaseCellViewModel extends Disposable { this._onDidChangeState.fire({ outputCollapsedChanged: true }); } - private _textEditorRestore: any; - constructor( readonly viewType: string, readonly model: NotebookCellTextModel, @@ -236,9 +234,7 @@ export abstract class BaseCellViewModel extends Disposable { this._textEditor = editor; if (this._editorViewStates) { - this._textEditorRestore = setTimeout(() => { - this._restoreViewState(this._editorViewStates); - }); + this._restoreViewState(this._editorViewStates); } if (this._editorTransientState) { @@ -264,7 +260,6 @@ export abstract class BaseCellViewModel extends Disposable { } detachTextEditor() { - clearTimeout(this._textEditorRestore); this.saveViewState(); this.saveTransientState(); // decorations need to be cleared first as editors can be resued. @@ -589,7 +584,6 @@ export abstract class BaseCellViewModel extends Disposable { super.dispose(); dispose(this._editorListeners); - clearTimeout(this._textEditorRestore); // Only remove the undo redo stack if we map this cell uri to itself // If we are not in perCell mode, it will map to the full NotebookDocument and diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorWidgetContextKeys.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorWidgetContextKeys.ts index a90477db542..b1cf70a4946 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorWidgetContextKeys.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorWidgetContextKeys.ts @@ -8,7 +8,7 @@ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/c import { ICellViewModel, INotebookEditorDelegate, KERNEL_EXTENSIONS } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NOTEBOOK_CELL_TOOLBAR_LOCATION, NOTEBOOK_HAS_OUTPUTS, NOTEBOOK_HAS_RUNNING_CELL, NOTEBOOK_INTERRUPTIBLE_KERNEL, NOTEBOOK_KERNEL, NOTEBOOK_KERNEL_COUNT, NOTEBOOK_KERNEL_SELECTED, NOTEBOOK_MISSING_KERNEL_EXTENSION, NOTEBOOK_USE_CONSOLIDATED_OUTPUT_BUTTON, NOTEBOOK_VIEW_TYPE } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; -import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { INotebookKernelService, NotebookKernelType } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; export class NotebookEditorContextKeys { @@ -148,7 +148,7 @@ export class NotebookEditorContextKeys { const { selected, all } = this._notebookKernelService.getMatchingKernel(this._editor.textModel); this._notebookKernelCount.set(all.length); - this._interruptibleKernel.set(selected?.implementsInterrupt ?? false); + this._interruptibleKernel.set((selected?.type === NotebookKernelType.Resolved && selected.implementsInterrupt) ?? false); this._notebookKernelSelected.set(Boolean(selected)); this._notebookKernel.set(selected?.id ?? ''); } diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelActionViewItem.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelActionViewItem.ts index dc9a494193e..e5814ce98ec 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelActionViewItem.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelActionViewItem.ts @@ -6,10 +6,11 @@ import 'vs/css!./notebookKernelActionViewItem'; import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { Action, IAction } from 'vs/base/common/actions'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { selectKernelIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; -import { INotebookKernelMatchResult, INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { INotebookKernelMatchResult, INotebookKernelService, NotebookKernelType, ProxyKernelState } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { Event } from 'vs/base/common/event'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; @@ -17,6 +18,7 @@ import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookB export class NotebooKernelActionViewItem extends ActionViewItem { private _kernelLabel?: HTMLAnchorElement; + private _kernelDisposable: DisposableStore; constructor( actualAction: IAction, @@ -31,6 +33,7 @@ export class NotebooKernelActionViewItem extends ActionViewItem { this._register(_editor.onDidChangeModel(this._update, this)); this._register(_notebookKernelService.onDidChangeNotebookAffinity(this._update, this)); this._register(_notebookKernelService.onDidChangeSelectedNotebooks(this._update, this)); + this._kernelDisposable = this._register(new DisposableStore()); } override render(container: HTMLElement): void { @@ -63,9 +66,9 @@ export class NotebooKernelActionViewItem extends ActionViewItem { } private _updateActionFromKernelInfo(info: INotebookKernelMatchResult): void { - + this._kernelDisposable.clear(); this._action.enabled = true; - const selectedOrSuggested = info.selected ?? (info.all.length === 1 && info.suggestions.length === 1 ? info.suggestions[0] : undefined); + const selectedOrSuggested = info.selected ?? ((info.suggestions.length === 1 && info.suggestions[0].type === NotebookKernelType.Resolved) ? info.suggestions[0] : undefined); if (selectedOrSuggested) { // selected or suggested kernel this._action.label = selectedOrSuggested.label; @@ -74,6 +77,23 @@ export class NotebooKernelActionViewItem extends ActionViewItem { // special UI for selected kernel? } + if (selectedOrSuggested.type === NotebookKernelType.Proxy) { + if (selectedOrSuggested.connectionState === ProxyKernelState.Initializing) { + this._action.label = localize('initializing', "Initializing..."); + } else { + this._action.label = selectedOrSuggested.label; + } + + this._kernelDisposable.add(selectedOrSuggested.onDidChange(e => { + if (e.connectionState) { + if (selectedOrSuggested.connectionState === ProxyKernelState.Initializing) { + this._action.label = localize('initializing', "Initializing..."); + } else { + this._action.label = selectedOrSuggested.label; + } + } + })); + } } else { // many kernels or no kernels this._action.label = localize('select', "Select Kernel"); diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index bc1f504dc36..28ff3f4f885 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -25,8 +25,7 @@ import { IEditorModel } from 'vs/platform/editor/common/editor'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { ThemeColor } from 'vs/platform/theme/common/themeService'; import { UndoRedoGroup } from 'vs/platform/undoRedo/common/undoRedo'; -import { IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor'; -import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { IRevertOptions, ISaveOptions, IUntypedEditorInput } from 'vs/workbench/common/editor'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { IWorkingCopyBackupMeta, IWorkingCopySaveEvent } from 'vs/workbench/services/workingCopy/common/workingCopy'; @@ -792,7 +791,7 @@ export interface INotebookEditorModel extends IEditorModel { hasAssociatedFilePath(): boolean; load(options?: INotebookLoadOptions): Promise<IResolvedNotebookEditorModel>; save(options?: ISaveOptions): Promise<boolean>; - saveAs(target: URI): Promise<EditorInput | undefined>; + saveAs(target: URI): Promise<IUntypedEditorInput | undefined>; revert(options?: IRevertOptions): Promise<void>; } @@ -925,7 +924,10 @@ export const NotebookSetting = { textOutputLineLimit: 'notebook.output.textLineLimit', globalToolbarShowLabel: 'notebook.globalToolbarShowLabel', markupFontSize: 'notebook.markup.fontSize', - interactiveWindowCollapseCodeCells: 'interactiveWindow.collapseCellInputCode' + interactiveWindowCollapseCodeCells: 'interactiveWindow.collapseCellInputCode', + outputLineHeight: 'notebook.outputLineHeight', + outputFontSize: 'notebook.outputFontSize', + outputFontFamily: 'notebook.outputFontFamily' } as const; export const enum CellStatusbarAlignment { diff --git a/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts b/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts index fd2279cba63..689f9ca995e 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts @@ -38,6 +38,8 @@ export const NOTEBOOK_CELL_EXECUTING = new RawContextKey<boolean>('notebookCellE export const NOTEBOOK_CELL_HAS_OUTPUTS = new RawContextKey<boolean>('notebookCellHasOutputs', false); export const NOTEBOOK_CELL_INPUT_COLLAPSED = new RawContextKey<boolean>('notebookCellInputIsCollapsed', false); export const NOTEBOOK_CELL_OUTPUT_COLLAPSED = new RawContextKey<boolean>('notebookCellOutputIsCollapsed', false); +export const NOTEBOOK_CELL_RESOURCE = new RawContextKey<string>('notebookCellResource', ''); + // Kernels export const NOTEBOOK_KERNEL = new RawContextKey<string>('notebookKernel', undefined); export const NOTEBOOK_KERNEL_COUNT = new RawContextKey<number>('notebookKernelCount', 0); diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts index cffd2b0362d..b8cbd4985d0 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts @@ -102,6 +102,10 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { } } + if (!(capabilities & EditorInputCapabilities.Readonly)) { + capabilities |= EditorInputCapabilities.CanDropIntoEditor; + } + return capabilities; } @@ -120,7 +124,7 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { return this._editorModelReference.object.isDirty(); } - override async save(group: GroupIdentifier, options?: ISaveOptions): Promise<EditorInput | undefined> { + override async save(group: GroupIdentifier, options?: ISaveOptions): Promise<EditorInput | IUntypedEditorInput | undefined> { if (this._editorModelReference) { if (this.hasCapability(EditorInputCapabilities.Untitled)) { @@ -135,7 +139,7 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { return undefined; } - override async saveAs(group: GroupIdentifier, options?: ISaveOptions): Promise<EditorInput | undefined> { + override async saveAs(group: GroupIdentifier, options?: ISaveOptions): Promise<IUntypedEditorInput | undefined> { if (!this._editorModelReference) { return undefined; } diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts index 8938cff2e80..7cd345036a7 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts @@ -4,8 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import { IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor'; -import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { IRevertOptions, ISaveOptions, IUntypedEditorInput } from 'vs/workbench/common/editor'; import { EditorModel } from 'vs/workbench/common/editor/editorModel'; import { Emitter, Event } from 'vs/base/common/event'; import { ICellDto2, INotebookEditorModel, INotebookLoadOptions, IResolvedNotebookEditorModel, NotebookCellsChangeType, NotebookData, NotebookDocumentBackupData } from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -28,8 +27,6 @@ import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/commo import { StoredFileWorkingCopyState, IStoredFileWorkingCopy, IStoredFileWorkingCopyModel, IStoredFileWorkingCopyModelContentChangedEvent, IStoredFileWorkingCopyModelFactory, IStoredFileWorkingCopySaveEvent } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopy'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { CancellationError } from 'vs/base/common/errors'; -import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { filter } from 'vs/base/common/objects'; import { IFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager'; import { IUntitledFileWorkingCopy, IUntitledFileWorkingCopyModel, IUntitledFileWorkingCopyModelContentChangedEvent, IUntitledFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopy'; @@ -59,7 +56,6 @@ export class ComplexNotebookEditorModel extends EditorModel implements INotebook readonly resource: URI, readonly viewType: string, private readonly _contentProvider: INotebookContentProvider, - @IInstantiationService private readonly _instantiationService: IInstantiationService, @INotebookService private readonly _notebookService: INotebookService, @IWorkingCopyService private readonly _workingCopyService: IWorkingCopyService, @IWorkingCopyBackupService private readonly _workingCopyBackupService: IWorkingCopyBackupService, @@ -393,7 +389,7 @@ export class ComplexNotebookEditorModel extends EditorModel implements INotebook }); } - async saveAs(targetResource: URI): Promise<EditorInput | undefined> { + async saveAs(targetResource: URI): Promise<IUntypedEditorInput | undefined> { if (!this.isResolved()) { return undefined; @@ -419,7 +415,7 @@ export class ComplexNotebookEditorModel extends EditorModel implements INotebook } this.setDirty(false); this._onDidSave.fire({}); - return this._instantiationService.createInstance(NotebookEditorInput, targetResource, this.viewType, {}); + return { resource: targetResource }; } private async _resolveStats(resource: URI) { @@ -462,7 +458,6 @@ export class SimpleNotebookEditorModel extends EditorModel implements INotebookE private readonly _hasAssociatedFilePath: boolean, readonly viewType: string, private readonly _workingCopyManager: IFileWorkingCopyManager<NotebookFileWorkingCopyModel, NotebookFileWorkingCopyModel>, - @IInstantiationService private readonly _instantiationService: IInstantiationService, @IFileService private readonly _fileService: IFileService ) { super(); @@ -547,14 +542,14 @@ export class SimpleNotebookEditorModel extends EditorModel implements INotebookE return this; } - async saveAs(target: URI): Promise<EditorInput | undefined> { + async saveAs(target: URI): Promise<IUntypedEditorInput | undefined> { const newWorkingCopy = await this._workingCopyManager.saveAs(this.resource, target); if (!newWorkingCopy) { return undefined; } // this is a little hacky because we leave the new working copy alone. BUT // the newly created editor input will pick it up and claim ownership of it. - return this._instantiationService.createInstance(NotebookEditorInput, newWorkingCopy.resource, this.viewType, {}); + return { resource: newWorkingCopy.resource }; } private static _isStoredFileWorkingCopy(candidate?: IStoredFileWorkingCopy<NotebookFileWorkingCopyModel> | IUntitledFileWorkingCopy<NotebookFileWorkingCopyModel>): candidate is IStoredFileWorkingCopy<NotebookFileWorkingCopyModel> { diff --git a/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts b/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts index 6610fe7177d..4f5304600b0 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts @@ -31,8 +31,13 @@ export interface INotebookKernelChangeEvent { hasExecutionOrder?: true; } -export interface INotebookKernel { +export const enum NotebookKernelType { + Resolved, + Proxy = 1 +} +export interface IResolvedNotebookKernel { + readonly type: NotebookKernelType.Resolved; readonly id: string; readonly viewType: string; readonly onDidChange: Event<Readonly<INotebookKernelChangeEvent>>; @@ -54,6 +59,34 @@ export interface INotebookKernel { cancelNotebookCellExecution(uri: URI, cellHandles: number[]): Promise<void>; } +export const enum ProxyKernelState { + Disconnected = 1, + Connected = 2, + Initializing = 3 +} + +export interface INotebookProxyKernelChangeEvent extends INotebookKernelChangeEvent { + connectionState?: true; +} + +export interface INotebookProxyKernel { + readonly type: NotebookKernelType.Proxy; + readonly id: string; + readonly viewType: string; + readonly extension: ExtensionIdentifier; + readonly preloadProvides: string[]; + readonly onDidChange: Event<Readonly<INotebookProxyKernelChangeEvent>>; + label: string; + description?: string; + detail?: string; + kind?: string; + supportedLanguages: string[]; + connectionState: ProxyKernelState; + resolveKernel(uri: URI): Promise<string | null>; +} + +export type INotebookKernel = IResolvedNotebookKernel | INotebookProxyKernel; + export interface INotebookTextModelLike { uri: URI; viewType: string } export const INotebookKernelService = createDecorator<INotebookKernelService>('INotebookKernelService'); diff --git a/src/vs/workbench/contrib/notebook/common/notebookOptions.ts b/src/vs/workbench/contrib/notebook/common/notebookOptions.ts index 9564a563def..426871f84fa 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookOptions.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookOptions.ts @@ -62,6 +62,9 @@ export interface NotebookLayoutConfiguration { showFoldingControls: 'always' | 'mouseover'; dragAndDropEnabled: boolean; fontSize: number; + outputFontSize: number; + outputFontFamily: string; + outputLineHeight: number; markupFontSize: number; focusIndicatorLeftMargin: number; editorOptionsCustomizations: any | undefined; @@ -84,9 +87,13 @@ export interface NotebookOptionsChangeEvent { readonly consolidatedRunButton?: boolean; readonly dragAndDropEnabled?: boolean; readonly fontSize?: boolean; + readonly outputFontSize?: boolean; readonly markupFontSize?: boolean; + readonly fontFamily?: boolean; + readonly outputFontFamily?: boolean; readonly editorOptionsCustomizations?: boolean; readonly interactiveWindowCollapseCodeCells?: boolean; + readonly outputLineHeight?: boolean; } const defaultConfigConstants = Object.freeze({ @@ -134,9 +141,12 @@ export class NotebookOptions extends Disposable { const showFoldingControls = this._computeShowFoldingControlsOption(); // const { bottomToolbarGap, bottomToolbarHeight } = this._computeBottomToolbarDimensions(compactView, insertToolbarPosition, insertToolbarAlignment); const fontSize = this.configurationService.getValue<number>('editor.fontSize'); + const outputFontSize = this.configurationService.getValue<number>(NotebookSetting.outputFontSize); + const outputFontFamily = this.configurationService.getValue<string>(NotebookSetting.outputFontFamily); const markupFontSize = this.configurationService.getValue<number>(NotebookSetting.markupFontSize); const editorOptionsCustomizations = this.configurationService.getValue(NotebookSetting.cellEditorOptionsCustomizations); const interactiveWindowCollapseCodeCells: InteractiveWindowCollapseCodeCells = this.configurationService.getValue(NotebookSetting.interactiveWindowCollapseCodeCells); + const outputLineHeight = this._computeOutputLineHeight(); this._layoutConfiguration = { ...(compactView ? compactConfigConstants : defaultConfigConstants), @@ -166,6 +176,9 @@ export class NotebookOptions extends Disposable { insertToolbarAlignment, showFoldingControls, fontSize, + outputFontSize, + outputFontFamily, + outputLineHeight, markupFontSize, editorOptionsCustomizations, focusIndicatorGap: 3, @@ -185,6 +198,29 @@ export class NotebookOptions extends Disposable { })); } + private _computeOutputLineHeight(): number { + const minimumLineHeight = 8; + let lineHeight = this.configurationService.getValue<number>(NotebookSetting.outputLineHeight); + + if (lineHeight < minimumLineHeight) { + // Values too small to be line heights in pixels are in ems. + let fontSize = this.configurationService.getValue<number>(NotebookSetting.outputFontSize); + if (fontSize === 0) { + fontSize = this.configurationService.getValue<number>('editor.fontSize'); + } + + lineHeight = lineHeight * fontSize; + } + + // Enforce integer, minimum constraints + lineHeight = Math.round(lineHeight); + if (lineHeight < minimumLineHeight) { + lineHeight = minimumLineHeight; + } + + return lineHeight; + } + private _updateConfiguration(e: IConfigurationChangeEvent) { const cellStatusBarVisibility = e.affectsConfiguration(NotebookSetting.showCellStatusBar); const cellToolbarLocation = e.affectsConfiguration(NotebookSetting.cellToolbarLocation); @@ -199,9 +235,13 @@ export class NotebookOptions extends Disposable { const showFoldingControls = e.affectsConfiguration(NotebookSetting.showFoldingControls); const dragAndDropEnabled = e.affectsConfiguration(NotebookSetting.dragAndDropEnabled); const fontSize = e.affectsConfiguration('editor.fontSize'); + const outputFontSize = e.affectsConfiguration(NotebookSetting.outputFontSize); const markupFontSize = e.affectsConfiguration(NotebookSetting.markupFontSize); + const fontFamily = e.affectsConfiguration('editor.fontFamily'); + const outputFontFamily = e.affectsConfiguration(NotebookSetting.outputFontFamily); const editorOptionsCustomizations = e.affectsConfiguration(NotebookSetting.cellEditorOptionsCustomizations); const interactiveWindowCollapseCodeCells = e.affectsConfiguration(NotebookSetting.interactiveWindowCollapseCodeCells); + const outputLineHeight = e.affectsConfiguration(NotebookSetting.outputLineHeight); if ( !cellStatusBarVisibility @@ -217,9 +257,13 @@ export class NotebookOptions extends Disposable { && !showFoldingControls && !dragAndDropEnabled && !fontSize + && !outputFontSize && !markupFontSize + && !fontFamily + && !outputFontFamily && !editorOptionsCustomizations - && !interactiveWindowCollapseCodeCells) { + && !interactiveWindowCollapseCodeCells + && !outputLineHeight) { return; } @@ -281,10 +325,18 @@ export class NotebookOptions extends Disposable { configuration.fontSize = this.configurationService.getValue<number>('editor.fontSize'); } + if (outputFontSize) { + configuration.outputFontSize = this.configurationService.getValue<number>(NotebookSetting.outputFontSize) ?? configuration.fontSize; + } + if (markupFontSize) { configuration.markupFontSize = this.configurationService.getValue<number>(NotebookSetting.markupFontSize); } + if (outputFontFamily) { + configuration.outputFontFamily = this.configurationService.getValue<string>(NotebookSetting.outputFontFamily); + } + if (editorOptionsCustomizations) { configuration.editorOptionsCustomizations = this.configurationService.getValue(NotebookSetting.cellEditorOptionsCustomizations); } @@ -293,6 +345,10 @@ export class NotebookOptions extends Disposable { configuration.interactiveWindowCollapseCodeCells = this.configurationService.getValue(NotebookSetting.interactiveWindowCollapseCodeCells); } + if (outputLineHeight || fontSize || outputFontSize) { + configuration.outputLineHeight = this._computeOutputLineHeight(); + } + this._layoutConfiguration = Object.freeze(configuration); // trigger event @@ -310,9 +366,13 @@ export class NotebookOptions extends Disposable { consolidatedRunButton, dragAndDropEnabled, fontSize, + outputFontSize, markupFontSize, + fontFamily, + outputFontFamily, editorOptionsCustomizations, - interactiveWindowCollapseCodeCells + interactiveWindowCollapseCodeCells, + outputLineHeight }); } @@ -502,7 +562,10 @@ export class NotebookOptions extends Disposable { runGutter: this._layoutConfiguration.cellRunGutter, dragAndDropEnabled: this._layoutConfiguration.dragAndDropEnabled, fontSize: this._layoutConfiguration.fontSize, + outputFontSize: this._layoutConfiguration.outputFontSize, + outputFontFamily: this._layoutConfiguration.outputFontFamily, markupFontSize: this._layoutConfiguration.markupFontSize, + outputLineHeight: this._layoutConfiguration.outputLineHeight, }; } @@ -517,7 +580,10 @@ export class NotebookOptions extends Disposable { runGutter: 0, dragAndDropEnabled: false, fontSize: this._layoutConfiguration.fontSize, + outputFontSize: this._layoutConfiguration.outputFontSize, + outputFontFamily: this._layoutConfiguration.outputFontFamily, markupFontSize: this._layoutConfiguration.markupFontSize, + outputLineHeight: this._layoutConfiguration.outputLineHeight, }; } diff --git a/src/vs/workbench/contrib/notebook/test/browser/contrib/find.test.ts b/src/vs/workbench/contrib/notebook/test/browser/contrib/find.test.ts index b9822f87393..6b1282d9afb 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/contrib/find.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/contrib/find.test.ts @@ -68,11 +68,11 @@ suite('Notebook Find', () => { await found; assert.strictEqual(model.findMatches.length, 2); assert.strictEqual(model.currentMatch, -1); - model.find(false); + model.find({ previous: false }); assert.strictEqual(model.currentMatch, 0); - model.find(false); + model.find({ previous: false }); assert.strictEqual(model.currentMatch, 1); - model.find(false); + model.find({ previous: false }); assert.strictEqual(model.currentMatch, 0); assert.strictEqual(editor.textModel.length, 3); @@ -115,11 +115,11 @@ suite('Notebook Find', () => { // find matches is not necessarily find results assert.strictEqual(model.findMatches.length, 4); assert.strictEqual(model.currentMatch, -1); - model.find(false); + model.find({ previous: false }); assert.strictEqual(model.currentMatch, 0); - model.find(false); + model.find({ previous: false }); assert.strictEqual(model.currentMatch, 1); - model.find(false); + model.find({ previous: false }); assert.strictEqual(model.currentMatch, 2); const found2 = new Promise<boolean>(resolve => state.onFindReplaceStateChange(e => { @@ -132,13 +132,13 @@ suite('Notebook Find', () => { assert.strictEqual(model.findMatches.length, 3); assert.strictEqual(model.currentMatch, 2); - model.find(true); + model.find({ previous: true }); assert.strictEqual(model.currentMatch, 1); - model.find(false); + model.find({ previous: false }); assert.strictEqual(model.currentMatch, 2); - model.find(false); + model.find({ previous: false }); assert.strictEqual(model.currentMatch, 3); - model.find(false); + model.find({ previous: false }); assert.strictEqual(model.currentMatch, 0); }); }); @@ -166,7 +166,7 @@ suite('Notebook Find', () => { // find matches is not necessarily find results assert.strictEqual(model.findMatches.length, 4); assert.strictEqual(model.currentMatch, -1); - model.find(true); + model.find({ previous: true }); assert.strictEqual(model.currentMatch, 4); const found2 = new Promise<boolean>(resolve => state.onFindReplaceStateChange(e => { @@ -178,9 +178,9 @@ suite('Notebook Find', () => { await found2; assert.strictEqual(model.findMatches.length, 3); assert.strictEqual(model.currentMatch, 0); - model.find(true); + model.find({ previous: true }); assert.strictEqual(model.currentMatch, 3); - model.find(true); + model.find({ previous: true }); assert.strictEqual(model.currentMatch, 2); }); }); @@ -208,9 +208,9 @@ suite('Notebook Find', () => { // find matches is not necessarily find results assert.strictEqual(model.findMatches.length, 4); assert.strictEqual(model.currentMatch, -1); - model.find(false); - model.find(false); - model.find(false); + model.find({ previous: false }); + model.find({ previous: false }); + model.find({ previous: false }); assert.strictEqual(model.currentMatch, 2); const found2 = new Promise<boolean>(resolve => state.onFindReplaceStateChange(e => { if (e.matchesCount) { resolve(true); } @@ -244,11 +244,11 @@ suite('Notebook Find', () => { await found; assert.strictEqual(model.findMatches.length, 2); assert.strictEqual(model.currentMatch, -1); - model.find(false); + model.find({ previous: false }); assert.strictEqual(model.currentMatch, 0); - model.find(false); + model.find({ previous: false }); assert.strictEqual(model.currentMatch, 1); - model.find(false); + model.find({ previous: false }); assert.strictEqual(model.currentMatch, 0); assert.strictEqual(editor.textModel.length, 3); diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookEditorModel.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookEditorModel.test.ts index d71a25076b3..124729d29b4 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookEditorModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookEditorModel.test.ts @@ -10,7 +10,6 @@ import { isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { mock } from 'vs/base/test/common/mock'; import { IFileService } from 'vs/platform/files/common/files'; -import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; import { ILabelService } from 'vs/platform/label/common/label'; import { NullLogService } from 'vs/platform/log/common/log'; import { INotificationService } from 'vs/platform/notification/common/notification'; @@ -187,7 +186,6 @@ suite('NotebookFileWorkingCopyModel', function () { suite('ComplexNotebookEditorModel', function () { - const instaService = new InstantiationService(); const notebokService = new class extends mock<INotebookService>() { }; const backupService = new class extends mock<IWorkingCopyBackupService>() { }; const notificationService = new class extends mock<INotificationService>() { }; @@ -214,8 +212,8 @@ suite('ComplexNotebookEditorModel', function () { } }; - new ComplexNotebookEditorModel(r1, 'fff', notebookDataProvider, instaService, notebokService, workingCopyService, backupService, fileService, notificationService, new NullLogService(), untitledTextEditorService, labelService); - new ComplexNotebookEditorModel(r2, 'fff', notebookDataProvider, instaService, notebokService, workingCopyService, backupService, fileService, notificationService, new NullLogService(), untitledTextEditorService, labelService); + new ComplexNotebookEditorModel(r1, 'fff', notebookDataProvider, notebokService, workingCopyService, backupService, fileService, notificationService, new NullLogService(), untitledTextEditorService, labelService); + new ComplexNotebookEditorModel(r2, 'fff', notebookDataProvider, notebokService, workingCopyService, backupService, fileService, notificationService, new NullLogService(), untitledTextEditorService, labelService); assert.strictEqual(copies.length, 2); assert.strictEqual(!isEqual(copies[0].resource, copies[1].resource), true); diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionService.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionService.test.ts index 6f65268830f..8d3bff7a83a 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionService.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionService.test.ts @@ -20,7 +20,7 @@ import { NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewMod import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { CellKind, IOutputDto, NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; -import { INotebookKernel, INotebookKernelService, ISelectedNotebooksChangeEvent } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { INotebookKernelService, IResolvedNotebookKernel, ISelectedNotebooksChangeEvent, NotebookKernelType } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { setupInstantiationService, withTestNotebook as _withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; @@ -166,7 +166,8 @@ suite('NotebookExecutionService', () => { }); }); -class TestNotebookKernel implements INotebookKernel { +class TestNotebookKernel implements IResolvedNotebookKernel { + type: NotebookKernelType.Resolved = NotebookKernelType.Resolved; id: string = 'test'; label: string = ''; viewType = '*'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionStateService.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionStateService.test.ts index 887a4a05443..e5b7f84b8bf 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionStateService.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionStateService.test.ts @@ -21,7 +21,7 @@ import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/no import { CellEditType, CellKind, CellUri, IOutputDto, NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookExecutionService } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; -import { INotebookKernel, INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { INotebookKernelService, IResolvedNotebookKernel, NotebookKernelType } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { setupInstantiationService, withTestNotebook as _withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; @@ -170,7 +170,8 @@ suite('NotebookExecutionStateService', () => { }); }); -class TestNotebookKernel implements INotebookKernel { +class TestNotebookKernel implements IResolvedNotebookKernel { + type: NotebookKernelType.Resolved = NotebookKernelType.Resolved; id: string = 'test'; label: string = ''; viewType = '*'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookKernelService.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookKernelService.test.ts index b1ebabc1db2..62d6afecb8b 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookKernelService.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookKernelService.test.ts @@ -8,7 +8,7 @@ import { URI } from 'vs/base/common/uri'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { setupInstantiationService, withTestNotebook as _withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; import { Emitter, Event } from 'vs/base/common/event'; -import { INotebookKernel, INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { INotebookKernelService, IResolvedNotebookKernel, NotebookKernelType } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { NotebookKernelService } from 'vs/workbench/contrib/notebook/browser/notebookKernelServiceImpl'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { mock } from 'vs/base/test/common/mock'; @@ -159,7 +159,8 @@ suite('NotebookKernelService', () => { }); }); -class TestNotebookKernel implements INotebookKernel { +class TestNotebookKernel implements IResolvedNotebookKernel { + type: NotebookKernelType.Resolved = NotebookKernelType.Resolved; id: string = Math.random() + 'kernel'; label: string = 'test-label'; viewType = '*'; diff --git a/src/vs/workbench/contrib/output/browser/logViewer.ts b/src/vs/workbench/contrib/output/browser/logViewer.ts index a542e7dbe91..3c78aa114d1 100644 --- a/src/vs/workbench/contrib/output/browser/logViewer.ts +++ b/src/vs/workbench/contrib/output/browser/logViewer.ts @@ -15,8 +15,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { TextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; import { URI } from 'vs/base/common/uri'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import { LOG_SCHEME } from 'vs/workbench/contrib/output/common/output'; -import { IFileOutputChannelDescriptor } from 'vs/workbench/services/output/common/output'; +import { LOG_SCHEME, IFileOutputChannelDescriptor } from 'vs/workbench/services/output/common/output'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; diff --git a/src/vs/workbench/contrib/output/browser/output.contribution.ts b/src/vs/workbench/contrib/output/browser/output.contribution.ts index b6dbbb8caf7..f9e4814794e 100644 --- a/src/vs/workbench/contrib/output/browser/output.contribution.ts +++ b/src/vs/workbench/contrib/output/browser/output.contribution.ts @@ -12,7 +12,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { MenuId, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { OutputService, LogContentProvider } from 'vs/workbench/contrib/output/browser/outputServices'; -import { OUTPUT_MODE_ID, OUTPUT_MIME, OUTPUT_VIEW_ID, IOutputService, CONTEXT_IN_OUTPUT, LOG_SCHEME, LOG_MODE_ID, LOG_MIME, CONTEXT_ACTIVE_LOG_OUTPUT, CONTEXT_OUTPUT_SCROLL_LOCK } from 'vs/workbench/contrib/output/common/output'; +import { OUTPUT_MODE_ID, OUTPUT_MIME, OUTPUT_VIEW_ID, IOutputService, CONTEXT_IN_OUTPUT, LOG_SCHEME, LOG_MODE_ID, LOG_MIME, CONTEXT_ACTIVE_LOG_OUTPUT, CONTEXT_OUTPUT_SCROLL_LOCK, IOutputChannelDescriptor, IFileOutputChannelDescriptor } from 'vs/workbench/services/output/common/output'; import { OutputViewPane } from 'vs/workbench/contrib/output/browser/outputView'; import { IEditorPaneRegistry, EditorPaneDescriptor } from 'vs/workbench/browser/editor'; import { LogViewer, LogViewerInput } from 'vs/workbench/contrib/output/browser/logViewer'; @@ -25,7 +25,6 @@ import { ViewContainer, IViewContainersRegistry, ViewContainerLocation, Extensio import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { IQuickPickItem, IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; -import { IOutputChannelDescriptor, IFileOutputChannelDescriptor } from 'vs/workbench/services/output/common/output'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { assertIsDefined } from 'vs/base/common/types'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; diff --git a/src/vs/workbench/contrib/output/browser/outputLinkProvider.ts b/src/vs/workbench/contrib/output/browser/outputLinkProvider.ts index eb346086655..ca06b37e21f 100644 --- a/src/vs/workbench/contrib/output/browser/outputLinkProvider.ts +++ b/src/vs/workbench/contrib/output/browser/outputLinkProvider.ts @@ -8,7 +8,7 @@ import { RunOnceScheduler } from 'vs/base/common/async'; import { IModelService } from 'vs/editor/common/services/model'; import { ILink } from 'vs/editor/common/languages'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { OUTPUT_MODE_ID, LOG_MODE_ID } from 'vs/workbench/contrib/output/common/output'; +import { OUTPUT_MODE_ID, LOG_MODE_ID } from 'vs/workbench/services/output/common/output'; import { MonacoWebWorker, createWebWorker } from 'vs/editor/browser/services/webWorker'; import { ICreateData, OutputLinkComputer } from 'vs/workbench/contrib/output/common/outputLinkComputer'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; diff --git a/src/vs/workbench/contrib/output/browser/outputServices.ts b/src/vs/workbench/contrib/output/browser/outputServices.ts index 4901c9f23ce..dffb7046911 100644 --- a/src/vs/workbench/contrib/output/browser/outputServices.ts +++ b/src/vs/workbench/contrib/output/browser/outputServices.ts @@ -9,8 +9,7 @@ import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { Registry } from 'vs/platform/registry/common/platform'; -import { IOutputChannel, IOutputService, OUTPUT_VIEW_ID, OUTPUT_SCHEME, LOG_SCHEME, LOG_MIME, OUTPUT_MIME, OutputChannelUpdateMode } from 'vs/workbench/contrib/output/common/output'; -import { IOutputChannelDescriptor, Extensions, IOutputChannelRegistry } from 'vs/workbench/services/output/common/output'; +import { IOutputChannel, IOutputService, OUTPUT_VIEW_ID, OUTPUT_SCHEME, LOG_SCHEME, LOG_MIME, OUTPUT_MIME, OutputChannelUpdateMode, IOutputChannelDescriptor, Extensions, IOutputChannelRegistry } from 'vs/workbench/services/output/common/output'; import { OutputLinkProvider } from 'vs/workbench/contrib/output/browser/outputLinkProvider'; import { ITextModelService, ITextModelContentProvider } from 'vs/editor/common/services/resolverService'; import { ITextModel } from 'vs/editor/common/model'; diff --git a/src/vs/workbench/contrib/output/browser/outputView.ts b/src/vs/workbench/contrib/output/browser/outputView.ts index ee9c7eadf5d..545d41fdcee 100644 --- a/src/vs/workbench/contrib/output/browser/outputView.ts +++ b/src/vs/workbench/contrib/output/browser/outputView.ts @@ -14,7 +14,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IEditorOpenContext } from 'vs/workbench/common/editor'; import { AbstractTextResourceEditor } from 'vs/workbench/browser/parts/editor/textResourceEditor'; -import { OUTPUT_VIEW_ID, IOutputService, CONTEXT_IN_OUTPUT, IOutputChannel, CONTEXT_ACTIVE_LOG_OUTPUT, CONTEXT_OUTPUT_SCROLL_LOCK } from 'vs/workbench/contrib/output/common/output'; +import { OUTPUT_VIEW_ID, IOutputService, CONTEXT_IN_OUTPUT, IOutputChannel, CONTEXT_ACTIVE_LOG_OUTPUT, CONTEXT_OUTPUT_SCROLL_LOCK, IOutputChannelDescriptor, IOutputChannelRegistry, Extensions } from 'vs/workbench/services/output/common/output'; import { IThemeService, registerThemingParticipant, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; @@ -27,7 +27,6 @@ import { IContextMenuService, IContextViewService } from 'vs/platform/contextvie import { IViewDescriptorService } from 'vs/workbench/common/views'; import { TextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { IOutputChannelDescriptor, IOutputChannelRegistry, Extensions } from 'vs/workbench/services/output/common/output'; import { Registry } from 'vs/platform/registry/common/platform'; import { attachSelectBoxStyler, attachStylerCallback } from 'vs/platform/theme/common/styler'; import { ISelectOptionItem } from 'vs/base/browser/ui/selectBox/selectBox'; diff --git a/src/vs/workbench/contrib/output/common/outputChannelModel.ts b/src/vs/workbench/contrib/output/common/outputChannelModel.ts index cb24f441e21..4c01e67797c 100644 --- a/src/vs/workbench/contrib/output/common/outputChannelModel.ts +++ b/src/vs/workbench/contrib/output/common/outputChannelModel.ts @@ -21,7 +21,7 @@ import { Range } from 'vs/editor/common/core/range'; import { VSBuffer } from 'vs/base/common/buffer'; import { ILogger, ILoggerService, ILogService } from 'vs/platform/log/common/log'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import { OutputChannelUpdateMode } from 'vs/workbench/contrib/output/common/output'; +import { OutputChannelUpdateMode } from 'vs/workbench/services/output/common/output'; export interface IOutputChannelModel extends IDisposable { readonly onDispose: Event<void>; diff --git a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css index 7145fe94aa9..a09f097b305 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css +++ b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css @@ -39,7 +39,7 @@ .settings-editor > .settings-header > .search-container > .settings-count-widget { position: absolute; - right: 35px; + right: 46px; top: 0px; margin: 4px 0px; } @@ -55,7 +55,7 @@ top: 0; right: 0; height: 100%; - width: 30px; + width: 43px; } .settings-editor > .settings-header > .search-container > .settings-clear-widget .action-label { diff --git a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts index 42365a36d4f..b5230a5af22 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts @@ -34,7 +34,7 @@ import { ConfigureLanguageBasedSettingsAction } from 'vs/workbench/contrib/prefe import { SettingsEditorContribution } from 'vs/workbench/contrib/preferences/browser/preferencesEditor'; import { preferencesOpenSettingsIcon } from 'vs/workbench/contrib/preferences/browser/preferencesIcons'; import { SettingsEditor2, SettingsFocusContext } from 'vs/workbench/contrib/preferences/browser/settingsEditor2'; -import { CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDINGS_SEARCH_FOCUS, CONTEXT_KEYBINDING_FOCUS, CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_JSON_EDITOR, CONTEXT_SETTINGS_ROW_FOCUS, CONTEXT_SETTINGS_SEARCH_FOCUS, CONTEXT_TOC_ROW_FOCUS, CONTEXT_WHEN_FOCUS, KEYBINDINGS_EDITOR_COMMAND_ADD, KEYBINDINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, KEYBINDINGS_EDITOR_COMMAND_COPY, KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND, KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND_TITLE, KEYBINDINGS_EDITOR_COMMAND_DEFINE, KEYBINDINGS_EDITOR_COMMAND_DEFINE_WHEN, KEYBINDINGS_EDITOR_COMMAND_FOCUS_KEYBINDINGS, KEYBINDINGS_EDITOR_COMMAND_RECORD_SEARCH_KEYS, KEYBINDINGS_EDITOR_COMMAND_REMOVE, KEYBINDINGS_EDITOR_COMMAND_RESET, KEYBINDINGS_EDITOR_COMMAND_SEARCH, KEYBINDINGS_EDITOR_COMMAND_SHOW_SIMILAR, KEYBINDINGS_EDITOR_COMMAND_SORTBY_PRECEDENCE, KEYBINDINGS_EDITOR_SHOW_DEFAULT_KEYBINDINGS, KEYBINDINGS_EDITOR_SHOW_EXTENSION_KEYBINDINGS, KEYBINDINGS_EDITOR_SHOW_USER_KEYBINDINGS, MODIFIED_SETTING_TAG, REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG, SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU } from 'vs/workbench/contrib/preferences/common/preferences'; +import { CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDINGS_SEARCH_FOCUS, CONTEXT_KEYBINDING_FOCUS, CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_JSON_EDITOR, CONTEXT_SETTINGS_ROW_FOCUS, CONTEXT_SETTINGS_SEARCH_FOCUS, CONTEXT_TOC_ROW_FOCUS, CONTEXT_WHEN_FOCUS, KEYBINDINGS_EDITOR_COMMAND_ADD, KEYBINDINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, KEYBINDINGS_EDITOR_COMMAND_COPY, KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND, KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND_TITLE, KEYBINDINGS_EDITOR_COMMAND_DEFINE, KEYBINDINGS_EDITOR_COMMAND_DEFINE_WHEN, KEYBINDINGS_EDITOR_COMMAND_FOCUS_KEYBINDINGS, KEYBINDINGS_EDITOR_COMMAND_RECORD_SEARCH_KEYS, KEYBINDINGS_EDITOR_COMMAND_REMOVE, KEYBINDINGS_EDITOR_COMMAND_RESET, KEYBINDINGS_EDITOR_COMMAND_SEARCH, KEYBINDINGS_EDITOR_COMMAND_SHOW_SIMILAR, KEYBINDINGS_EDITOR_COMMAND_SORTBY_PRECEDENCE, KEYBINDINGS_EDITOR_SHOW_DEFAULT_KEYBINDINGS, KEYBINDINGS_EDITOR_SHOW_EXTENSION_KEYBINDINGS, KEYBINDINGS_EDITOR_SHOW_USER_KEYBINDINGS, REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG, SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU } from 'vs/workbench/contrib/preferences/common/preferences'; import { PreferencesContribution } from 'vs/workbench/contrib/preferences/common/preferencesContribution'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -54,7 +54,6 @@ const SETTINGS_EDITOR_COMMAND_FOCUS_CONTROL = 'settings.action.focusSettingContr const SETTINGS_EDITOR_COMMAND_FOCUS_UP = 'settings.action.focusLevelUp'; const SETTINGS_EDITOR_COMMAND_SWITCH_TO_JSON = 'settings.switchToJSON'; -const SETTINGS_EDITOR_COMMAND_FILTER_MODIFIED = 'settings.filterByModified'; const SETTINGS_EDITOR_COMMAND_FILTER_ONLINE = 'settings.filterByOnline'; const SETTINGS_EDITOR_COMMAND_FILTER_TELEMETRY = 'settings.filterByTelemetry'; const SETTINGS_EDITOR_COMMAND_FILTER_UNTRUSTED = 'settings.filterUntrusted'; @@ -396,33 +395,12 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon registerAction2(class extends Action2 { constructor() { super({ - id: SETTINGS_EDITOR_COMMAND_FILTER_MODIFIED, - title: { value: nls.localize('filterModifiedLabel', "Show modified settings"), original: 'Show modified settings' }, - menu: { - id: MenuId.EditorTitle, - group: '1_filter', - order: 1, - when: ContextKeyExpr.and(CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_JSON_EDITOR.toNegated()) - } - }); - } - run(accessor: ServicesAccessor, resource: URI) { - const editorPane = accessor.get(IEditorService).activeEditorPane; - if (editorPane instanceof SettingsEditor2) { - editorPane.focusSearch(`@${MODIFIED_SETTING_TAG}`); - } - } - }); - registerAction2(class extends Action2 { - constructor() { - super({ id: SETTINGS_EDITOR_COMMAND_FILTER_ONLINE, - title: { value: nls.localize('filterOnlineServicesLabel', "Show settings for online services"), original: 'Show settings for online services' }, + title: nls.localize({ key: 'miOpenOnlineSettings', comment: ['&& denotes a mnemonic'] }, "&&Online Services Settings"), menu: { - id: MenuId.EditorTitle, - group: '1_filter', + id: MenuId.MenubarPreferencesMenu, + group: '1_settings', order: 2, - when: ContextKeyExpr.and(CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_JSON_EDITOR.toNegated()) } }); } @@ -435,22 +413,6 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon } } }); - MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { - group: '1_settings', - command: { - id: SETTINGS_EDITOR_COMMAND_FILTER_ONLINE, - title: nls.localize({ key: 'miOpenOnlineSettings', comment: ['&& denotes a mnemonic'] }, "&&Online Services Settings") - }, - order: 2 - }); - MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { - group: '2_configuration', - command: { - id: SETTINGS_EDITOR_COMMAND_FILTER_ONLINE, - title: nls.localize('onlineServices', "Online Services Settings") - }, - order: 2 - }); registerAction2(class extends Action2 { constructor() { diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesIcons.ts b/src/vs/workbench/contrib/preferences/browser/preferencesIcons.ts index 6692bf47bd4..0c31b455aaf 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesIcons.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesIcons.ts @@ -25,4 +25,5 @@ export const settingsRemoveIcon = registerIcon('settings-remove', Codicon.close, export const settingsDiscardIcon = registerIcon('settings-discard', Codicon.discard, localize('preferencesDiscardIcon', 'Icon for the discard action in the Settings UI.')); export const preferencesClearInputIcon = registerIcon('preferences-clear-input', Codicon.clearAll, localize('preferencesClearInput', 'Icon for clear input in the Settings and keybinding UI.')); +export const preferencesFilterIcon = registerIcon('preferences-filter', Codicon.filter, localize('settingsFilter', 'Icon for the button that suggests filters for the Settings UI.')); export const preferencesOpenSettingsIcon = registerIcon('preferences-open-settings', Codicon.goToFile, localize('preferencesOpenSettings', 'Icon for open settings commands.')); diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts b/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts index fbddbef9008..8f833f178f6 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts @@ -19,7 +19,7 @@ import { IExtensionManagementService, ILocalExtension } from 'vs/platform/extens import { IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ILogService } from 'vs/platform/log/common/log'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { canceled } from 'vs/base/common/errors'; +import { CancellationError } from 'vs/base/common/errors'; import { ExtensionType } from 'vs/platform/extensions/common/extensions'; import { nullRange } from 'vs/workbench/services/preferences/common/preferencesModels'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -191,7 +191,7 @@ class RemoteSearchProvider implements ISearchProvider { } if (token && token.isCancellationRequested) { - throw canceled(); + throw new CancellationError(); } const resultKeys = Object.keys(remoteResult.scoredResults); @@ -379,7 +379,7 @@ class RemoteSearchProvider implements ISearchProvider { const hasMoreFilters = filters.length > (filterPage + 1) * RemoteSearchProvider.MAX_REQUEST_FILTERS; const body = JSON.stringify({ - query: encodedQuery, + search: encodedQuery, filters: encodeURIComponent(filterStr), rawQuery: encodeURIComponent(verbatimQuery) }); diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts b/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts index 9ec6a0b1083..4c942330f52 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts @@ -36,6 +36,7 @@ import { PANEL_ACTIVE_TITLE_BORDER, PANEL_ACTIVE_TITLE_FOREGROUND, PANEL_INACTIV import { settingsEditIcon, settingsScopeDropDownIcon } from 'vs/workbench/contrib/preferences/browser/preferencesIcons'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; +import { ILanguageService } from 'vs/editor/common/languages/language'; export class FolderSettingsActionViewItem extends BaseActionViewItem { @@ -216,6 +217,7 @@ export class SettingsTargetsWidget extends Widget { private userLocalSettings!: Action; private userRemoteSettings!: Action; private workspaceSettings!: Action; + private folderSettingsAction!: Action; private folderSettings!: FolderSettingsActionViewItem; private options: ISettingsTargetsWidgetOptions; @@ -232,6 +234,7 @@ export class SettingsTargetsWidget extends Widget { @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @ILabelService private readonly labelService: ILabelService, @IPreferencesService private readonly preferencesService: IPreferencesService, + @ILanguageService private readonly languageService: ILanguageService, ) { super(); this.options = options || {}; @@ -240,6 +243,15 @@ export class SettingsTargetsWidget extends Widget { this._register(this.contextService.onDidChangeWorkspaceFolders(() => this.update())); } + private resetLabels() { + const remoteAuthority = this.environmentService.remoteAuthority; + const hostLabel = remoteAuthority && this.labelService.getHostLabel(Schemas.vscodeRemote, remoteAuthority); + this.userLocalSettings.label = localize('userSettings', "User"); + this.userRemoteSettings.label = localize('userSettingsRemote', "Remote") + (hostLabel ? ` [${hostLabel}]` : ''); + this.workspaceSettings.label = localize('workspaceSettings', "Workspace"); + this.folderSettingsAction.label = localize('folderSettings', "Folder"); + } + private create(parent: HTMLElement): void { const settingsTabsWidget = DOM.append(parent, DOM.$('.settings-tabs-widget')); this.settingsSwitcherBar = this._register(new ActionBar(settingsTabsWidget, { @@ -250,31 +262,28 @@ export class SettingsTargetsWidget extends Widget { actionViewItemProvider: (action: IAction) => action.id === 'folderSettings' ? this.folderSettings : undefined })); - this.userLocalSettings = new Action('userSettings', localize('userSettings', "User"), '.settings-tab', true, () => this.updateTarget(ConfigurationTarget.USER_LOCAL)); + this.userLocalSettings = new Action('userSettings', '', '.settings-tab', true, () => this.updateTarget(ConfigurationTarget.USER_LOCAL)); this.preferencesService.getEditableSettingsURI(ConfigurationTarget.USER_LOCAL).then(uri => { // Don't wait to create UI on resolving remote this.userLocalSettings.tooltip = uri?.fsPath || ''; }); - const remoteAuthority = this.environmentService.remoteAuthority; - const hostLabel = remoteAuthority && this.labelService.getHostLabel(Schemas.vscodeRemote, remoteAuthority); - const remoteSettingsLabel = localize('userSettingsRemote', "Remote") + - (hostLabel ? ` [${hostLabel}]` : ''); - this.userRemoteSettings = new Action('userSettingsRemote', remoteSettingsLabel, '.settings-tab', true, () => this.updateTarget(ConfigurationTarget.USER_REMOTE)); + this.userRemoteSettings = new Action('userSettingsRemote', '', '.settings-tab', true, () => this.updateTarget(ConfigurationTarget.USER_REMOTE)); this.preferencesService.getEditableSettingsURI(ConfigurationTarget.USER_REMOTE).then(uri => { this.userRemoteSettings.tooltip = uri?.fsPath || ''; }); - this.workspaceSettings = new Action('workspaceSettings', localize('workspaceSettings', "Workspace"), '.settings-tab', false, () => this.updateTarget(ConfigurationTarget.WORKSPACE)); + this.workspaceSettings = new Action('workspaceSettings', '', '.settings-tab', false, () => this.updateTarget(ConfigurationTarget.WORKSPACE)); - const folderSettingsAction = new Action('folderSettings', localize('folderSettings', "Folder"), '.settings-tab', false, async folder => { + this.folderSettingsAction = new Action('folderSettings', '', '.settings-tab', false, async folder => { this.updateTarget(isWorkspaceFolder(folder) ? folder.uri : ConfigurationTarget.USER_LOCAL); }); - this.folderSettings = this.instantiationService.createInstance(FolderSettingsActionViewItem, folderSettingsAction); + this.folderSettings = this.instantiationService.createInstance(FolderSettingsActionViewItem, this.folderSettingsAction); + this.resetLabels(); this.update(); - this.settingsSwitcherBar.push([this.userLocalSettings, this.userRemoteSettings, this.workspaceSettings, folderSettingsAction]); + this.settingsSwitcherBar.push([this.userLocalSettings, this.userRemoteSettings, this.workspaceSettings, this.folderSettingsAction]); } get settingsTarget(): SettingsTarget | null { @@ -314,6 +323,19 @@ export class SettingsTargetsWidget extends Widget { } } + updateLanguageFilterIndicators(filter: string | undefined) { + this.resetLabels(); + if (filter) { + const languageToUse = this.languageService.getLanguageName(filter); + if (languageToUse) { + this.userLocalSettings.label += ` [${languageToUse}]`; + this.userRemoteSettings.label += ` [${languageToUse}]`; + this.workspaceSettings.label += ` [${languageToUse}]`; + this.folderSettingsAction.label += ` [${languageToUse}]`; + } + } + } + private onWorkbenchStateChanged(): void { this.folderSettings.folder = null; this.update(); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index 77942bbbf0a..abdaa391f5a 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -43,14 +43,14 @@ import { commonlyUsedData, tocData } from 'vs/workbench/contrib/preferences/brow import { AbstractSettingRenderer, HeightChangeParams, ISettingLinkClickEvent, ISettingOverrideClickEvent, resolveConfiguredUntrustedSettings, createTocTreeForExtensionSettings, resolveSettingsTree, SettingsTree, SettingTreeRenderers } from 'vs/workbench/contrib/preferences/browser/settingsTree'; import { ISettingsEditorViewState, parseQuery, SearchResultIdx, SearchResultModel, SettingsTreeElement, SettingsTreeGroupChild, SettingsTreeGroupElement, SettingsTreeModel, SettingsTreeSettingElement } from 'vs/workbench/contrib/preferences/browser/settingsTreeModels'; import { createTOCIterator, TOCTree, TOCTreeModel } from 'vs/workbench/contrib/preferences/browser/tocTree'; -import { CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_ROW_FOCUS, CONTEXT_SETTINGS_SEARCH_FOCUS, CONTEXT_TOC_ROW_FOCUS, ENABLE_LANGUAGE_FILTER, EXTENSION_SETTING_TAG, FEATURE_SETTING_TAG, ID_SETTING_TAG, IPreferencesSearchService, ISearchProvider, LANGUAGE_SETTING_TAG, MODIFIED_SETTING_TAG, REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG, SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, WORKSPACE_TRUST_SETTING_TAG } from 'vs/workbench/contrib/preferences/common/preferences'; +import { CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_ROW_FOCUS, CONTEXT_SETTINGS_SEARCH_FOCUS, CONTEXT_TOC_ROW_FOCUS, ENABLE_LANGUAGE_FILTER, EXTENSION_SETTING_TAG, FEATURE_SETTING_TAG, ID_SETTING_TAG, IPreferencesSearchService, ISearchProvider, LANGUAGE_SETTING_TAG, MODIFIED_SETTING_TAG, REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG, SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, SETTINGS_EDITOR_COMMAND_SUGGEST_FILTERS, WORKSPACE_TRUST_SETTING_TAG } from 'vs/workbench/contrib/preferences/common/preferences'; import { settingsHeaderBorder, settingsSashBorder, settingsTextInputBorder } from 'vs/workbench/contrib/preferences/common/settingsEditorColorRegistry'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IOpenSettingsOptions, IPreferencesService, ISearchResult, ISettingsEditorModel, ISettingsEditorOptions, SettingMatchType, SettingValueType, validateSettingsEditorOptions } from 'vs/workbench/services/preferences/common/preferences'; import { SettingsEditor2Input } from 'vs/workbench/services/preferences/common/preferencesEditorInput'; import { Settings2EditorModel } from 'vs/workbench/services/preferences/common/preferencesModels'; import { IUserDataSyncWorkbenchService } from 'vs/workbench/services/userDataSync/common/userDataSync'; -import { preferencesClearInputIcon } from 'vs/workbench/contrib/preferences/browser/preferencesIcons'; +import { preferencesClearInputIcon, preferencesFilterIcon } from 'vs/workbench/contrib/preferences/browser/preferencesIcons'; import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; import { IWorkbenchConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; @@ -58,6 +58,8 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { Orientation, Sizing, SplitView } from 'vs/base/browser/ui/splitview/splitview'; import { Color } from 'vs/base/common/color'; import { ILanguageService } from 'vs/editor/common/languages/language'; +import { SettingsSearchFilterDropdownMenuActionViewItem } from 'vs/workbench/contrib/preferences/browser/settingsSearchMenu'; +import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; export const enum SettingsFocusContext { Search, @@ -201,6 +203,8 @@ export class SettingsEditor2 extends EditorPane { private settingsTreeScrollTop = 0; private dimension!: DOM.Dimension; + private installedExtensionIds: string[] = []; + constructor( @ITelemetryService telemetryService: ITelemetryService, @IWorkbenchConfigurationService private readonly configurationService: IWorkbenchConfigurationService, @@ -217,7 +221,8 @@ export class SettingsEditor2 extends EditorPane { @IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService, @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, @IExtensionService private readonly extensionService: IExtensionService, - @ILanguageService private readonly languageService: ILanguageService + @ILanguageService private readonly languageService: ILanguageService, + @IExtensionManagementService extensionManagementService: IExtensionManagementService ) { super(SettingsEditor2.ID, telemetryService, themeService, storageService); this.delayedFilterLogging = new Delayer<void>(1000); @@ -268,6 +273,12 @@ export class SettingsEditor2 extends EditorPane { if (ENABLE_LANGUAGE_FILTER && !SettingsEditor2.SUGGESTIONS.includes(`@${LANGUAGE_SETTING_TAG}`)) { SettingsEditor2.SUGGESTIONS.push(`@${LANGUAGE_SETTING_TAG}`); } + + extensionManagementService.getInstalled().then(extensions => { + this.installedExtensionIds = extensions + .filter(ext => ext.manifest && ext.manifest.contributes && ext.manifest.contributes.configuration) + .map(ext => ext.identifier.id); + }); } override get minimumWidth(): number { return SettingsEditor2.EDITOR_MIN_WIDTH; } @@ -499,11 +510,11 @@ export class SettingsEditor2 extends EditorPane { clearSearchFilters(): void { let query = this.searchWidget.getValue(); - SettingsEditor2.SUGGESTIONS.forEach(suggestion => { - query = query.replace(suggestion, ''); + const splitQuery = query.split(' ').filter(word => { + return word.length && !SettingsEditor2.SUGGESTIONS.some(suggestion => word.startsWith(suggestion)); }); - this.searchWidget.setValue(query.trim()); + this.searchWidget.setValue(splitQuery.join(' ')); } private updateInputAriaLabel() { @@ -525,7 +536,7 @@ export class SettingsEditor2 extends EditorPane { const searchContainer = DOM.append(this.headerContainer, $('.search-container')); const clearInputAction = new Action(SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, localize('clearInput', "Clear Settings Search Input"), ThemeIcon.asClassName(preferencesClearInputIcon), false, async () => this.clearSearchResults()); - + const filterAction = new Action(SETTINGS_EDITOR_COMMAND_SUGGEST_FILTERS, localize('filterInput', "Filter Settings"), ThemeIcon.asClassName(preferencesFilterIcon)); this.searchWidget = this._register(this.instantiationService.createInstance(SuggestEnabledInput, `${SettingsEditor2.ID}.searchbox`, searchContainer, { triggerCharacters: ['@', ':'], provideResults: (query: string) => { @@ -533,9 +544,15 @@ export class SettingsEditor2 extends EditorPane { // for the ':' trigger, only return suggestions if there was a '@' before it in the same word. const queryParts = query.split(/\s/g); if (queryParts[queryParts.length - 1].startsWith(`@${LANGUAGE_SETTING_TAG}`)) { - return this.languageService.getRegisteredLanguageIds().map(languageId => { + const sortedLanguages = this.languageService.getRegisteredLanguageIds().map(languageId => { return `@${LANGUAGE_SETTING_TAG}${languageId} `; }).sort(); + return sortedLanguages.filter(langFilter => !query.includes(langFilter)); + } else if (queryParts[queryParts.length - 1].startsWith(`@${EXTENSION_SETTING_TAG}`)) { + const installedExtensionsTags = this.installedExtensionIds.map(extensionId => { + return `@${EXTENSION_SETTING_TAG}${extensionId} `; + }).sort(); + return installedExtensionsTags.filter(extFilter => !query.includes(extFilter)); } else if (queryParts[queryParts.length - 1].startsWith('@')) { return SettingsEditor2.SUGGESTIONS.filter(tag => !query.includes(tag)).map(tag => tag.endsWith(':') ? tag : tag + ' '); } @@ -603,10 +620,15 @@ export class SettingsEditor2 extends EditorPane { const actionBar = this._register(new ActionBar(this.controlsElement, { animated: false, - actionViewItemProvider: (_action) => { return undefined; } + actionViewItemProvider: (action) => { + if (action.id === filterAction.id) { + return this.instantiationService.createInstance(SettingsSearchFilterDropdownMenuActionViewItem, action, this.actionRunner, this.searchWidget); + } + return undefined; + } })); - actionBar.push([clearInputAction], { label: false, icon: true }); + actionBar.push([clearInputAction, filterAction], { label: false, icon: true }); } private onDidSettingsTargetChange(target: SettingsTarget): void { @@ -831,7 +853,11 @@ export class SettingsEditor2 extends EditorPane { } })); this._register(this.settingRenderers.onApplyLanguageFilter((lang: string) => { - this.focusSearch(`@${LANGUAGE_SETTING_TAG}${lang}`); + if (this.searchWidget) { + // Prepend the language filter to the query. + const newQuery = `@${LANGUAGE_SETTING_TAG}${lang} ${this.searchWidget.getValue().trimStart()}`; + this.focusSearch(newQuery, false); + } })); this.settingsTree = this._register(this.instantiationService.createInstance(SettingsTree, @@ -1018,13 +1044,15 @@ export class SettingsEditor2 extends EditorPane { target: string; }; type SettingsEditorModifiedSettingClassification = { - key: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'rzhao271'; comment: 'The setting that is being modified.' }; - groupId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'rzhao271'; comment: 'Whether the setting is from the local search or remote search provider, if applicable.' }; - nlpIndex: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; owner: 'rzhao271'; comment: 'The index of the setting in the remote search provider results, if applicable.' }; - displayIndex: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; owner: 'rzhao271'; comment: 'The index of the setting in the combined search results, if applicable.' }; - showConfiguredOnly: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; 'owner': 'rzhao271'; comment: 'Whether the user is in the modified view, which shows configured settings only.' }; - isReset: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'rzhao271'; comment: 'Identifies whether a setting was reset to its default value.' }; - target: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'rzhao271'; comment: 'The scope of the setting, such as user or workspace.' }; + key: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The setting that is being modified.' }; + groupId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the setting is from the local search or remote search provider, if applicable.' }; + nlpIndex: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The index of the setting in the remote search provider results, if applicable.' }; + displayIndex: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The index of the setting in the combined search results, if applicable.' }; + showConfiguredOnly: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the user is in the modified view, which shows configured settings only.' }; + isReset: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Identifies whether a setting was reset to its default value.' }; + target: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The scope of the setting, such as user or workspace.' }; + owner: 'rzhao271'; + comment: 'Event which fires when the user modifies a setting in the settings editor'; }; this.pendingSettingUpdate = null; @@ -1304,6 +1332,8 @@ export class SettingsEditor2 extends EditorPane { this.viewState.languageFilter = parsedQuery.languageFilter; } + this.settingsTargetsWidget.updateLanguageFilterIndicators(this.viewState.languageFilter); + if (query && query !== '@') { query = this.parseSettingFromJSON(query) || query; return this.triggerFilterPreferences(query); @@ -1376,10 +1406,12 @@ export class SettingsEditor2 extends EditorPane { 'requestCount': number | undefined; }; type SettingsEditorFilterClassification = { - 'durations.nlpResult': { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; owner: 'rzhao271'; 'comment': 'How long the remote search provider took, if applicable.' }; - 'counts.nlpResult': { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; owner: 'rzhao271'; 'comment': 'The number of matches found by the remote search provider, if applicable.' }; - 'counts.filterResult': { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; owner: 'rzhao271'; 'comment': 'The number of matches found by the local search provider, if applicable.' }; - 'requestCount': { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; owner: 'rzhao271'; 'comment': 'The number of requests sent to Bing, if applicable.' }; + 'durations.nlpResult': { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; 'comment': 'How long the remote search provider took, if applicable.' }; + 'counts.nlpResult': { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; 'comment': 'The number of matches found by the remote search provider, if applicable.' }; + 'counts.filterResult': { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; 'comment': 'The number of matches found by the local search provider, if applicable.' }; + 'requestCount': { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; 'comment': 'The number of requests sent to Bing, if applicable.' }; + owner: 'rzhao271'; + comment: 'Tracks the number of requests and performance of the built-in search providers'; }; const nlpResult = results[SearchResultIdx.Remote]; diff --git a/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts b/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts index d631ff74b4a..9d2c8958ba0 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts @@ -145,7 +145,7 @@ export const tocData: ITOCEntry<string> = { }, { id: 'features/scm', - label: localize('scm', "SCM"), + label: localize('scm', "Source Control"), settings: ['scm.*'] }, { diff --git a/src/vs/workbench/contrib/preferences/browser/settingsSearchMenu.ts b/src/vs/workbench/contrib/preferences/browser/settingsSearchMenu.ts new file mode 100644 index 00000000000..e39d23eb58e --- /dev/null +++ b/src/vs/workbench/contrib/preferences/browser/settingsSearchMenu.ts @@ -0,0 +1,141 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview'; +import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem'; +import { IAction, IActionRunner } from 'vs/base/common/actions'; +import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController'; +import { localize } from 'vs/nls'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { SuggestEnabledInput } from 'vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput'; +import { EXTENSION_SETTING_TAG, FEATURE_SETTING_TAG, GENERAL_TAG_SETTING_TAG, LANGUAGE_SETTING_TAG, MODIFIED_SETTING_TAG } from 'vs/workbench/contrib/preferences/common/preferences'; + +export class SettingsSearchFilterDropdownMenuActionViewItem extends DropdownMenuActionViewItem { + private readonly suggestController: SuggestController | null; + + constructor( + action: IAction, + actionRunner: IActionRunner | undefined, + private readonly searchWidget: SuggestEnabledInput, + @IContextMenuService contextMenuService: IContextMenuService + ) { + super(action, + { getActions: () => this.getActions() }, + contextMenuService, + { + actionRunner, + classNames: action.class, + anchorAlignmentProvider: () => AnchorAlignment.RIGHT, + menuAsChild: true + } + ); + + this.suggestController = SuggestController.get(this.searchWidget.inputWidget); + } + + override render(container: HTMLElement): void { + super.render(container); + } + + private doSearchWidgetAction(queryToAppend: string, triggerSuggest: boolean) { + this.searchWidget.setValue(this.searchWidget.getValue().trimEnd() + ' ' + queryToAppend); + this.searchWidget.focus(); + if (triggerSuggest && this.suggestController) { + this.suggestController.triggerSuggest(); + } + } + + /** + * The created action appends a query to the search widget search string. It optionally triggers suggestions. + */ + private createAction(id: string, label: string, tooltip: string, queryToAppend: string, triggerSuggest: boolean): IAction { + return { + id, + label, + tooltip, + class: undefined, + enabled: true, + checked: false, + run: () => { this.doSearchWidgetAction(queryToAppend, triggerSuggest); }, + dispose: () => { } + }; + } + + /** + * The created action appends a query to the search widget search string, if the query does not exist. + * Otherwise, it removes the query from the search widget search string. + * The action does not trigger suggestions after adding or removing the query. + */ + private createToggleAction(id: string, label: string, tooltip: string, queryToAppend: string): IAction { + const splitCurrentQuery = this.searchWidget.getValue().split(' '); + const queryContainsQueryToAppend = splitCurrentQuery.includes(queryToAppend); + return { + id, + label, + tooltip, + class: undefined, + enabled: true, + checked: queryContainsQueryToAppend, + run: () => { + if (!queryContainsQueryToAppend) { + const trimmedCurrentQuery = this.searchWidget.getValue().trimEnd(); + const newQuery = trimmedCurrentQuery ? trimmedCurrentQuery + ' ' + queryToAppend : queryToAppend; + this.searchWidget.setValue(newQuery); + } else { + const queryWithRemovedTags = this.searchWidget.getValue().split(' ') + .filter(word => word !== queryToAppend).join(' '); + this.searchWidget.setValue(queryWithRemovedTags); + } + this.searchWidget.focus(); + }, + dispose: () => { } + }; + } + + getActions(): IAction[] { + return [ + this.createToggleAction( + 'modifiedSettingsSearch', + localize('modifiedSettingsSearch', "Modified"), + localize('modifiedSettingsSearchTooltip', "Add or remove modified settings filter"), + `@${MODIFIED_SETTING_TAG}` + ), + this.createAction( + 'extSettingsSearch', + localize('extSettingsSearch', "Extension ID..."), + localize('extSettingsSearchTooltip', "Add extension ID filter"), + `@${EXTENSION_SETTING_TAG}`, + true + ), + this.createAction( + 'featuresSettingsSearch', + localize('featureSettingsSearch', "Feature..."), + localize('featureSettingsSearchTooltip', "Add feature filter"), + `@${FEATURE_SETTING_TAG}`, + true + ), + this.createAction( + 'tagSettingsSearch', + localize('tagSettingsSearch', "Tag..."), + localize('tagSettingsSearchTooltip', "Add tag filter"), + `@${GENERAL_TAG_SETTING_TAG}`, + true + ), + this.createAction( + 'langSettingsSearch', + localize('langSettingsSearch', "Language..."), + localize('langSettingsSearchTooltip', "Add language ID filter"), + `@${LANGUAGE_SETTING_TAG}`, + true + ), + this.createToggleAction( + 'onlineSettingsSearch', + localize('onlineSettingsSearch', "Online services"), + localize('onlineSettingsSearchTooltip', "Show settings for online services"), + '@tag:usesOnlineServices' + ) + ]; + } +} diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index f4b4b41ad94..7b551ab1466 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -2234,13 +2234,6 @@ export class SettingsTreeFilter implements ITreeFilter<SettingsTreeElement> { } } - // @modified or tag - if (element instanceof SettingsTreeSettingElement && this.viewState.tagFilters) { - if (!element.matchesAllTags(this.viewState.tagFilters)) { - return false; - } - } - // Group with no visible children if (element instanceof SettingsTreeGroupElement) { if (typeof element.count === 'number') { diff --git a/src/vs/workbench/contrib/preferences/common/preferences.ts b/src/vs/workbench/contrib/preferences/common/preferences.ts index ec3a14f1bf4..6138098d36f 100644 --- a/src/vs/workbench/contrib/preferences/common/preferences.ts +++ b/src/vs/workbench/contrib/preferences/common/preferences.ts @@ -42,6 +42,7 @@ export interface ISearchProvider { export const SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS = 'settings.action.clearSearchResults'; export const SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU = 'settings.action.showContextMenu'; +export const SETTINGS_EDITOR_COMMAND_SUGGEST_FILTERS = 'settings.action.suggestFilters'; export const CONTEXT_SETTINGS_EDITOR = new RawContextKey<boolean>('inSettingsEditor', false); export const CONTEXT_SETTINGS_JSON_EDITOR = new RawContextKey<boolean>('inSettingsJSONEditor', false); @@ -76,6 +77,7 @@ export const EXTENSION_SETTING_TAG = 'ext:'; export const FEATURE_SETTING_TAG = 'feature:'; export const ID_SETTING_TAG = 'id:'; export const LANGUAGE_SETTING_TAG = 'lang:'; +export const GENERAL_TAG_SETTING_TAG = 'tag:'; export const WORKSPACE_TRUST_SETTING_TAG = 'workspaceTrust'; export const REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG = 'requireTrustedWorkspace'; export const KEYBOARD_LAYOUT_OPEN_PICKER = 'workbench.action.openKeyboardLayoutPicker'; diff --git a/src/vs/workbench/browser/parts/editor/media/binaryeditor.css b/src/vs/workbench/contrib/profiles/common/profiles.contribution.ts index d68e045cc4e..facfa51c4a3 100644 --- a/src/vs/workbench/browser/parts/editor/media/binaryeditor.css +++ b/src/vs/workbench/contrib/profiles/common/profiles.contribution.ts @@ -3,18 +3,4 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-binary-resource-editor:focus { - outline: none !important; -} - -.monaco-binary-resource-editor { - padding: 0 0 0 16px; - box-sizing: border-box; -} - -.monaco-binary-resource-editor .monaco-link, -.monaco-binary-resource-editor .monaco-link:hover { - cursor: pointer; - text-decoration: underline; - margin-left: 5px; -} +import './profilesActions'; diff --git a/src/vs/workbench/contrib/profiles/common/profilesActions.ts b/src/vs/workbench/contrib/profiles/common/profilesActions.ts new file mode 100644 index 00000000000..bae901d966d --- /dev/null +++ b/src/vs/workbench/contrib/profiles/common/profilesActions.ts @@ -0,0 +1,136 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from 'vs/base/common/cancellation'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { joinPath } from 'vs/base/common/resources'; +import { localize } from 'vs/nls'; +import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { IDialogService, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IFileService } from 'vs/platform/files/common/files'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { asJson, asText, IRequestService } from 'vs/platform/request/common/request'; +import { IProfile, isProfile, IWorkbenchProfileService, PROFILES_CATEGORY, PROFILE_EXTENSION, PROFILE_FILTER } from 'vs/workbench/services/profiles/common/profile'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; + +registerAction2(class ExportProfileAction extends Action2 { + constructor() { + super({ + id: 'workbench.profiles.actions.exportProfile', + title: { + value: localize('export profile', "Export Settings as a Profile..."), + original: 'Export Settings as a Profile...' + }, + category: PROFILES_CATEGORY, + f1: true + }); + } + + async run(accessor: ServicesAccessor) { + const textFileService = accessor.get(ITextFileService); + const fileDialogService = accessor.get(IFileDialogService); + const profileService = accessor.get(IWorkbenchProfileService); + const notificationService = accessor.get(INotificationService); + + const profileLocation = await fileDialogService.showSaveDialog({ + title: localize('export profile dialog', "Save Profile"), + filters: PROFILE_FILTER, + defaultUri: joinPath(await fileDialogService.defaultFilePath(), `profile.${PROFILE_EXTENSION}`), + }); + + if (!profileLocation) { + return; + } + + const profile = await profileService.createProfile({ skipComments: true }); + await textFileService.create([{ resource: profileLocation, value: JSON.stringify(profile), options: { overwrite: true } }]); + + notificationService.info(localize('export success', "{0}: Exported successfully.", PROFILES_CATEGORY)); + } +}); + +registerAction2(class ImportProfileAction extends Action2 { + constructor() { + super({ + id: 'workbench.profiles.actions.importProfile', + title: { + value: localize('import profile', "Import Settings from a Profile..."), + original: 'Import Settings from a Profile...' + }, + category: PROFILES_CATEGORY, + f1: true + }); + } + + async run(accessor: ServicesAccessor) { + const fileDialogService = accessor.get(IFileDialogService); + const quickInputService = accessor.get(IQuickInputService); + const fileService = accessor.get(IFileService); + const requestService = accessor.get(IRequestService); + const profileService = accessor.get(IWorkbenchProfileService); + const dialogService = accessor.get(IDialogService); + + if (!(await dialogService.confirm({ + title: localize('import profile title', "Import Settings from a Profile"), + message: localize('confiirmation message', "This will replace your current settings. Are you sure you want to continue?"), + })).confirmed) { + return; + } + + const disposables = new DisposableStore(); + const quickPick = disposables.add(quickInputService.createQuickPick()); + const updateQuickPickItems = (value?: string) => { + const selectFromFileItem: IQuickPickItem = { label: localize('select from file', "Import from profile file") }; + quickPick.items = value ? [{ label: localize('select from url', "Import from URL"), description: quickPick.value }, selectFromFileItem] : [selectFromFileItem]; + }; + quickPick.title = localize('import profile quick pick title', "Import Settings from a Profile"); + quickPick.placeholder = localize('import profile placeholder', "Provide profile URL or select profile file to import"); + quickPick.ignoreFocusOut = true; + disposables.add(quickPick.onDidChangeValue(updateQuickPickItems)); + updateQuickPickItems(); + quickPick.matchOnLabel = false; + quickPick.matchOnDescription = false; + disposables.add(quickPick.onDidAccept(async () => { + quickPick.hide(); + const profile = quickPick.selectedItems[0].description ? await this.getProfileFromURL(quickPick.value, requestService) : await this.getProfileFromFileSystem(fileDialogService, fileService); + if (profile) { + await profileService.setProfile(profile); + } + })); + disposables.add(quickPick.onDidHide(() => disposables.dispose())); + quickPick.show(); + } + + private async getProfileFromFileSystem(fileDialogService: IFileDialogService, fileService: IFileService): Promise<IProfile | null> { + const profileLocation = await fileDialogService.showOpenDialog({ + canSelectFolders: false, + canSelectFiles: true, + canSelectMany: false, + filters: PROFILE_FILTER, + title: localize('import profile dialog', "Import Profile"), + }); + if (!profileLocation) { + return null; + } + const content = (await fileService.readFile(profileLocation[0])).value.toString(); + const parsed = JSON.parse(content); + return isProfile(parsed) ? parsed : null; + } + + private async getProfileFromURL(url: string, requestService: IRequestService): Promise<IProfile | null> { + const options = { type: 'GET', url }; + const context = await requestService.request(options, CancellationToken.None); + if (context.res.statusCode === 200) { + const result = await asJson(context); + return isProfile(result) ? result : null; + } else { + const message = await asText(context); + throw new Error(`Expected 200, got back ${context.res.statusCode} instead.\n\n${message}`); + } + } + +}); diff --git a/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts b/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts index 6d2258b96f1..0f8b4edc30d 100644 --- a/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts +++ b/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts @@ -160,12 +160,17 @@ export class ShowAllCommandsAction extends Action2 { super({ id: ShowAllCommandsAction.ID, title: { value: localize('showTriggerActions', "Show All Commands"), original: 'Show All Commands' }, - f1: true, keybinding: { weight: KeybindingWeight.WorkbenchContrib, when: undefined, primary: !isFirefox ? (KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyP) : undefined, secondary: [KeyCode.F1] + }, + f1: true, + menu: { + id: MenuId.TitleMenuQuickPick, + group: '1/workspaceNav', + order: 3 } }); } @@ -188,9 +193,23 @@ export class ClearCommandHistoryAction extends Action2 { async run(accessor: ServicesAccessor): Promise<void> { const configurationService = accessor.get(IConfigurationService); const storageService = accessor.get(IStorageService); + const dialogService = accessor.get(IDialogService); const commandHistoryLength = CommandsHistory.getConfiguredCommandHistoryLength(configurationService); if (commandHistoryLength > 0) { + + // Ask for confirmation + const { confirmed } = await dialogService.confirm({ + message: localize('confirmClearMessage', "Do you want to clear the history of recently used commands?"), + detail: localize('confirmClearDetail', "This action is irreversible!"), + primaryButton: localize({ key: 'clearButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Clear"), + type: 'warning' + }); + + if (!confirmed) { + return; + } + CommandsHistory.clearHistory(configurationService, storageService); } } diff --git a/src/vs/workbench/contrib/quickaccess/browser/viewQuickAccess.ts b/src/vs/workbench/contrib/quickaccess/browser/viewQuickAccess.ts index d1a3d7680d5..4a278af2146 100644 --- a/src/vs/workbench/contrib/quickaccess/browser/viewQuickAccess.ts +++ b/src/vs/workbench/contrib/quickaccess/browser/viewQuickAccess.ts @@ -7,7 +7,7 @@ import { localize } from 'vs/nls'; import { IQuickPickSeparator, IQuickInputService, ItemActivation } from 'vs/platform/quickinput/common/quickInput'; import { IPickerQuickAccessItem, PickerQuickAccessProvider } from 'vs/platform/quickinput/browser/pickerQuickAccess'; import { IViewDescriptorService, IViewsService, ViewContainer, ViewContainerLocation } from 'vs/workbench/common/views'; -import { IOutputService } from 'vs/workbench/contrib/output/common/output'; +import { IOutputService } from 'vs/workbench/services/output/common/output'; import { ITerminalGroupService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { PaneCompositeDescriptor } from 'vs/workbench/browser/panecomposite'; diff --git a/src/vs/workbench/contrib/scm/browser/activity.ts b/src/vs/workbench/contrib/scm/browser/activity.ts index 399e48dcce8..38a53b72c60 100644 --- a/src/vs/workbench/contrib/scm/browser/activity.ts +++ b/src/vs/workbench/contrib/scm/browser/activity.ts @@ -18,6 +18,7 @@ import { EditorResourceAccessor } from 'vs/workbench/common/editor'; 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'; function getCount(repository: ISCMRepository): number { if (typeof repository.provider.count === 'number') { @@ -116,7 +117,7 @@ export class SCMStatusController implements IWorkbenchContribution { return; } - this.focusRepository(this.scmService.repositories[0]); + this.focusRepository(Iterable.first(this.scmService.repositories)); } private focusRepository(repository: ISCMRepository | undefined): void { @@ -148,7 +149,8 @@ export class SCMStatusController implements IWorkbenchContribution { : repository.provider.label; const disposables = new DisposableStore(); - for (const command of commands) { + for (let index = 0; index < commands.length; index++) { + const command = commands[index]; const tooltip = `${label}${command.tooltip ? ` - ${command.tooltip}` : ''}`; let ariaLabel = stripIcons(command.title).trim(); @@ -160,7 +162,7 @@ export class SCMStatusController implements IWorkbenchContribution { ariaLabel: `${ariaLabel}${command.tooltip ? ` - ${command.tooltip}` : ''}`, tooltip, command: command.id ? command : undefined - }, 'status.scm', MainThreadStatusBarAlignment.LEFT, 10000)); + }, `status.scm.${index}`, MainThreadStatusBarAlignment.LEFT, 10000)); } this.statusBarDisposable = disposables; @@ -172,7 +174,7 @@ export class SCMStatusController implements IWorkbenchContribution { let count = 0; if (countBadgeType === 'all') { - count = this.scmService.repositories.reduce((r, repository) => r + getCount(repository), 0); + count = Iterable.reduce(this.scmService.repositories, (r, repository) => r + getCount(repository), 0); } else if (countBadgeType === 'focused' && this.focusedRepository) { count = getCount(this.focusedRepository); } @@ -198,7 +200,7 @@ export class SCMStatusController implements IWorkbenchContribution { export class SCMActiveResourceContextKeyController implements IWorkbenchContribution { private activeResourceHasChangesContextKey: IContextKey<boolean>; - private activeResourceRepositoryContextKey: IContextKey<ISCMRepository | undefined>; + private activeResourceRepositoryContextKey: IContextKey<string | undefined>; private disposables = new DisposableStore(); private repositoryDisposables = new Set<IDisposable>(); @@ -239,11 +241,12 @@ export class SCMActiveResourceContextKeyController implements IWorkbenchContribu const activeResource = EditorResourceAccessor.getOriginalUri(this.editorService.activeEditor); if (activeResource?.scheme === Schemas.file || activeResource?.scheme === Schemas.vscodeRemote) { - const activeResourceRepository = this.scmService.repositories - .find(r => r.provider.rootUri && - this.uriIdentityService.extUri.isEqualOrParent(activeResource, r.provider.rootUri)); + const activeResourceRepository = Iterable.find( + this.scmService.repositories, + r => Boolean(r.provider.rootUri && this.uriIdentityService.extUri.isEqualOrParent(activeResource, r.provider.rootUri)) + ); - this.activeResourceRepositoryContextKey.set(activeResourceRepository); + this.activeResourceRepositoryContextKey.set(activeResourceRepository?.id); for (const resourceGroup of activeResourceRepository?.provider.groups.elements ?? []) { if (resourceGroup.elements diff --git a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts index 600b2af4b21..c05f0b2f4be 100644 --- a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts +++ b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts @@ -20,7 +20,7 @@ import { URI } from 'vs/base/common/uri'; import { ISCMService, ISCMRepository, ISCMProvider } from 'vs/workbench/contrib/scm/common/scm'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { registerThemingParticipant, IColorTheme, ICssStyleCollector, themeColorFromId, IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { editorBackground, editorErrorForeground, registerColor, transparent } from 'vs/platform/theme/common/colorRegistry'; +import { editorErrorForeground, registerColor, transparent } from 'vs/platform/theme/common/colorRegistry'; import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { registerEditorAction, registerEditorContribution, ServicesAccessor, EditorAction } from 'vs/editor/browser/editorExtensions'; import { PeekViewWidget, getOuterEditor, peekViewBorder, peekViewTitleBackground, peekViewTitleForeground, peekViewTitleInfoForeground } from 'vs/editor/contrib/peekView/browser/peekView'; @@ -52,6 +52,8 @@ import { TextCompareEditorActiveContext } from 'vs/workbench/common/contextkeys' import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; import { IChange } from 'vs/editor/common/diff/diffComputer'; import { Color } from 'vs/base/common/color'; +import { editorGutter } from 'vs/editor/common/core/editorColorRegistry'; +import { Iterable } from 'vs/base/common/iterator'; class DiffActionRunner extends ActionRunner { @@ -608,7 +610,8 @@ export class DirtyDiffController extends Disposable implements IEditorContributi } .monaco-editor .margin-view-overlays .dirty-diff-glyph:hover::before { - width: 9px; + height: 100%; + width: 6px; left: -6px; } @@ -923,8 +926,10 @@ class DirtyDiffDecorator extends Disposable { return ModelDecorationOptions.createDynamic(decorationOptions); } - private modifiedOptions: ModelDecorationOptions; private addedOptions: ModelDecorationOptions; + private addedPatternOptions: ModelDecorationOptions; + private modifiedOptions: ModelDecorationOptions; + private modifiedPatternOptions: ModelDecorationOptions; private deletedOptions: ModelDecorationOptions; private decorations: string[] = []; private editorModel: ITextModel | null; @@ -932,25 +937,38 @@ class DirtyDiffDecorator extends Disposable { constructor( editorModel: ITextModel, private model: DirtyDiffModel, - @IConfigurationService configurationService: IConfigurationService + @IConfigurationService private readonly configurationService: IConfigurationService ) { super(); this.editorModel = editorModel; + const decorations = configurationService.getValue<string>('scm.diffDecorations'); const gutter = decorations === 'all' || decorations === 'gutter'; const overview = decorations === 'all' || decorations === 'overview'; const minimap = decorations === 'all' || decorations === 'minimap'; + this.addedOptions = DirtyDiffDecorator.createDecoration('dirty-diff-added', { + gutter, + overview: { active: overview, color: overviewRulerAddedForeground }, + minimap: { active: minimap, color: minimapGutterAddedBackground }, + isWholeLine: true + }); + this.addedPatternOptions = DirtyDiffDecorator.createDecoration('dirty-diff-added-pattern', { + gutter, + overview: { active: overview, color: overviewRulerAddedForeground }, + minimap: { active: minimap, color: minimapGutterAddedBackground }, + isWholeLine: true + }); this.modifiedOptions = DirtyDiffDecorator.createDecoration('dirty-diff-modified', { gutter, overview: { active: overview, color: overviewRulerModifiedForeground }, minimap: { active: minimap, color: minimapGutterModifiedBackground }, isWholeLine: true }); - this.addedOptions = DirtyDiffDecorator.createDecoration('dirty-diff-added', { + this.modifiedPatternOptions = DirtyDiffDecorator.createDecoration('dirty-diff-modified-pattern', { gutter, - overview: { active: overview, color: overviewRulerAddedForeground }, - minimap: { active: minimap, color: minimapGutterAddedBackground }, + overview: { active: overview, color: overviewRulerModifiedForeground }, + minimap: { active: minimap, color: minimapGutterModifiedBackground }, isWholeLine: true }); this.deletedOptions = DirtyDiffDecorator.createDecoration('dirty-diff-deleted', { @@ -960,6 +978,12 @@ class DirtyDiffDecorator extends Disposable { isWholeLine: false }); + this._register(configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('scm.diffDecorationsGutterPattern')) { + this.onDidChange(); + } + })); + this._register(model.onDidChange(this.onDidChange, this)); } @@ -967,6 +991,8 @@ class DirtyDiffDecorator extends Disposable { if (!this.editorModel) { return; } + + const pattern = this.configurationService.getValue<{ added: boolean; modified: boolean }>('scm.diffDecorationsGutterPattern'); const decorations = this.model.changes.map((change) => { const changeType = getChangeType(change); const startLineNumber = change.modifiedStartLineNumber; @@ -979,7 +1005,7 @@ class DirtyDiffDecorator extends Disposable { startLineNumber: startLineNumber, startColumn: 1, endLineNumber: endLineNumber, endColumn: 1 }, - options: this.addedOptions + options: pattern.added ? this.addedPatternOptions : this.addedOptions }; case ChangeType.Delete: return { @@ -995,7 +1021,7 @@ class DirtyDiffDecorator extends Disposable { startLineNumber: startLineNumber, startColumn: 1, endLineNumber: endLineNumber, endColumn: 1 }, - options: this.modifiedOptions + options: pattern.modified ? this.modifiedPatternOptions : this.modifiedOptions }; } }); @@ -1055,8 +1081,8 @@ export function createProviderComparer(uri: URI): (a: ISCMProvider, b: ISCMProvi } export async function getOriginalResource(scmService: ISCMService, uri: URI): Promise<URI | null> { - const providers = scmService.repositories.map(r => r.provider); - const rootedProviders = providers.filter(p => !!p.rootUri); + const providers = Iterable.map(scmService.repositories, r => r.provider); + const rootedProviders = Iterable.collect(Iterable.filter(providers, p => !!p.rootUri)); rootedProviders.sort(createProviderComparer(uri)); @@ -1066,8 +1092,8 @@ export async function getOriginalResource(scmService: ISCMService, uri: URI): Pr return result; } - const nonRootedProviders = providers.filter(p => !p.rootUri); - return first(nonRootedProviders.map(p => () => p.getOriginalResource(uri))); + const nonRootedProviders = Iterable.filter(providers, p => !p.rootUri); + return first(Iterable.collect(Iterable.map(nonRootedProviders, p => () => p.getOriginalResource(uri)))); } export class DirtyDiffModel extends Disposable { @@ -1108,7 +1134,7 @@ export class DirtyDiffModel extends Disposable { )(this.triggerDiff, this) ); this._register(scmService.onDidAddRepository(this.onDidAddRepository, this)); - scmService.repositories.forEach(r => this.onDidAddRepository(r)); + Iterable.forEach(scmService.repositories, r => this.onDidAddRepository(r)); this._register(this._model.onDidChangeEncoding(() => { this.diffDelayer.cancel(); @@ -1371,9 +1397,21 @@ export class DirtyDiffWorkbenchController extends Disposable implements ext.IWor private setViewState(state: IViewState): void { this.viewState = state; this.stylesheet.textContent = ` - .monaco-editor .dirty-diff-modified { background-size: ${state.width}px 4.5px; } - .monaco-editor .dirty-diff-modified, .monaco-editor .dirty-diff-added{border-left-width:${state.width}px;} - .monaco-editor .dirty-diff-modified, .monaco-editor .dirty-diff-added, .monaco-editor .dirty-diff-deleted { + .monaco-editor .dirty-diff-added, + .monaco-editor .dirty-diff-modified { + border-left-width:${state.width}px; + } + .monaco-editor .dirty-diff-added-pattern, + .monaco-editor .dirty-diff-added-pattern:before, + .monaco-editor .dirty-diff-modified-pattern, + .monaco-editor .dirty-diff-modified-pattern:before { + background-size: ${state.width}px ${state.width}px; + } + .monaco-editor .dirty-diff-added, + .monaco-editor .dirty-diff-added-pattern, + .monaco-editor .dirty-diff-modified, + .monaco-editor .dirty-diff-modified-pattern, + .monaco-editor .dirty-diff-deleted { opacity: ${state.visibility === 'always' ? 1 : 0}; } `; @@ -1468,39 +1506,61 @@ export class DirtyDiffWorkbenchController extends Disposable implements ext.IWor registerEditorContribution(DirtyDiffController.ID, DirtyDiffController); registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { - const editorBackgroundColor = theme.getColor(editorBackground); + const editorGutterBackgroundColor = theme.getColor(editorGutter); const editorGutterModifiedBackgroundColor = theme.getColor(editorGutterModifiedBackground); - const linearGradient = `-45deg, ${editorGutterModifiedBackgroundColor} 25%, ${editorBackgroundColor} 25%, ${editorBackgroundColor} 50%, ${editorGutterModifiedBackgroundColor} 50%, ${editorGutterModifiedBackgroundColor} 75%, ${editorBackgroundColor} 75%, ${editorBackgroundColor}`; - if (editorGutterModifiedBackgroundColor) { + const getLinearGradient = (color: Color): string => { + return `-45deg, ${color} 25%, ${editorGutterBackgroundColor} 25%, ${editorGutterBackgroundColor} 50%, ${color} 50%, ${color} 75%, ${editorGutterBackgroundColor} 75%, ${editorGutterBackgroundColor}`; + }; + + if (editorGutterBackgroundColor && editorGutterModifiedBackgroundColor) { collector.addRule(` .monaco-editor .dirty-diff-modified { - background-repeat: repeat-y; - background-image: linear-gradient(${linearGradient}); + border-left-color: ${editorGutterModifiedBackgroundColor}; + border-left-style: solid; transition: opacity 0.5s; } .monaco-editor .dirty-diff-modified:before { + background: ${editorGutterModifiedBackgroundColor}; + } + .monaco-editor .dirty-diff-modified-pattern { + background-image: linear-gradient(${getLinearGradient(editorGutterModifiedBackgroundColor)}); + background-repeat: repeat-y; + transition: opacity 0.5s; + } + .monaco-editor .dirty-diff-modified-pattern:before { + background-image: linear-gradient(${getLinearGradient(editorGutterModifiedBackgroundColor)}); transform: translateX(3px); - background-size: 3px 3px; - background-image: linear-gradient(${linearGradient}); } - .monaco-editor .margin:hover .dirty-diff-modified { + .monaco-editor .margin:hover .dirty-diff-modified, + .monaco-editor .margin:hover .dirty-diff-modified-pattern { opacity: 1; } `); } const editorGutterAddedBackgroundColor = theme.getColor(editorGutterAddedBackground); - if (editorGutterAddedBackgroundColor) { + if (editorGutterBackgroundColor && editorGutterAddedBackgroundColor) { collector.addRule(` .monaco-editor .dirty-diff-added { - border-left: 3px solid ${editorGutterAddedBackgroundColor}; + border-left-color: ${editorGutterAddedBackgroundColor}; + border-left-style: solid; transition: opacity 0.5s; } .monaco-editor .dirty-diff-added:before { background: ${editorGutterAddedBackgroundColor}; } - .monaco-editor .margin:hover .dirty-diff-added { + .monaco-editor .dirty-diff-added-pattern { + background-image: linear-gradient(${getLinearGradient(editorGutterAddedBackgroundColor)}); + background-repeat: repeat-y; + transition: opacity 0.5s; + } + .monaco-editor .dirty-diff-added-pattern:before { + background-image: linear-gradient(${getLinearGradient(editorGutterAddedBackgroundColor)}); + transform: translateX(3px); + } + .monaco-editor .margin:hover .dirty-diff-added, + .monaco-editor .margin:hover .dirty-diff-added-pattern { opacity: 1; } `); diff --git a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts index 27fa26a947d..956e867a21f 100644 --- a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts +++ b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts @@ -7,7 +7,7 @@ import { localize } from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { DirtyDiffWorkbenchController } from './dirtydiffDecorator'; -import { VIEWLET_ID, ISCMRepository, ISCMService, VIEW_PANE_ID, ISCMProvider, ISCMViewService, REPOSITORIES_VIEW_PANE_ID } from 'vs/workbench/contrib/scm/common/scm'; +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, SCMStatusController } from './activity'; @@ -83,7 +83,7 @@ viewsRegistry.registerViews([{ containerIcon: sourceControlViewIcon, openCommandActionDescriptor: { id: viewContainer.id, - mnemonicTitle: localize({ key: 'miViewSCM', comment: ['&& denotes a mnemonic'] }, "S&&CM"), + mnemonicTitle: localize({ key: 'miViewSCM', comment: ['&& denotes a mnemonic'] }, "Source &&Control"), keybindings: { primary: 0, win: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyG }, @@ -118,7 +118,7 @@ Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench) Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).registerConfiguration({ id: 'scm', order: 5, - title: localize('scmConfigurationTitle', "SCM"), + title: localize('scmConfigurationTitle', "Source Control"), type: 'object', scope: ConfigurationScope.RESOURCE, properties: { @@ -161,6 +161,25 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).regis description: localize('scm.diffDecorationsGutterAction', "Controls the behavior of Source Control diff gutter decorations."), default: 'diff' }, + 'scm.diffDecorationsGutterPattern': { + type: 'object', + description: localize('diffGutterPattern', "Controls whether a pattern is used for the diff decorations in gutter."), + additionalProperties: false, + properties: { + 'added': { + type: 'boolean', + description: localize('diffGutterPatternAdded', "Use pattern for the diff decorations in gutter for added lines."), + }, + 'modified': { + type: 'boolean', + description: localize('diffGutterPatternModifed', "Use pattern for the diff decorations in gutter for modified lines."), + }, + }, + default: { + 'added': false, + 'modified': true + } + }, 'scm.diffDecorationsIgnoreTrimWhitespace': { type: 'string', enum: ['true', 'false', 'inherit'], @@ -215,14 +234,14 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).regis enumDescriptions: [ localize('scm.defaultViewSortKey.name', "Sort the repository changes by file name."), localize('scm.defaultViewSortKey.path', "Sort the repository changes by path."), - localize('scm.defaultViewSortKey.status', "Sort the repository changes by SCM status.") + localize('scm.defaultViewSortKey.status', "Sort the repository changes by Source Control status.") ], - description: localize('scm.defaultViewSortKey', "Controls the default Source Control repository sort mode."), + description: localize('scm.defaultViewSortKey', "Controls the default Source Control repository changes sort order when viewed as a list."), default: 'path' }, 'scm.autoReveal': { type: 'boolean', - description: localize('autoReveal', "Controls whether the SCM view should automatically reveal and select files when opening them."), + description: localize('autoReveal', "Controls whether the Source Control view should automatically reveal and select files when opening them."), default: true }, 'scm.inputFontFamily': { @@ -237,9 +256,20 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).regis }, 'scm.alwaysShowRepositories': { type: 'boolean', - markdownDescription: localize('alwaysShowRepository', "Controls whether repositories should always be visible in the SCM view."), + markdownDescription: localize('alwaysShowRepository', "Controls whether repositories should always be visible in the Source Control view."), default: false }, + 'scm.repositories.sortOrder': { + type: 'string', + enum: ['discovery time', 'name', 'path'], + enumDescriptions: [ + localize('scm.repositoriesSortOrder.discoveryTime', "Repositories in the Source Control Repositories view are sorted by discovery time. Repositories in the Source Control view are sorted in the order that they were selected."), + localize('scm.repositoriesSortOrder.name', "Repositories in the Source Control Repositories and Source Control views are sorted by repository name."), + localize('scm.repositoriesSortOrder.path', "Repositories in the Source Control Repositories and Source Control views are sorted by repository path.") + ], + description: localize('repositoriesSortOrder', "Controls the sort order of the repositories in the source control repositories view."), + default: 'discovery time' + }, 'scm.repositories.visible': { type: 'number', description: localize('providersVisible', "Controls how many repositories are visible in the Source Control Repositories section. Set to `0` to be able to manually resize the view."), @@ -247,7 +277,7 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).regis }, 'scm.showActionButton': { type: 'boolean', - markdownDescription: localize('showActionButton', "Controls whether an action button can be shown in the SCM view."), + markdownDescription: localize('showActionButton', "Controls whether an action button can be shown in the Source Control view."), default: true } } @@ -255,44 +285,56 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).regis KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'scm.acceptInput', - description: { description: localize('scm accept', "SCM: Accept Input"), args: [] }, + description: { description: localize('scm accept', "Source Control: Accept Input"), args: [] }, weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.has('scmRepository'), primary: KeyMod.CtrlCmd | KeyCode.Enter, handler: accessor => { const contextKeyService = accessor.get(IContextKeyService); const context = contextKeyService.getContext(document.activeElement); - const repository = context.getValue<ISCMRepository>('scmRepository'); + const repositoryId = context.getValue<string | undefined>('scmRepository'); + + if (!repositoryId) { + return Promise.resolve(null); + } + + const scmService = accessor.get(ISCMService); + const repository = scmService.getRepository(repositoryId); - if (!repository || !repository.provider.acceptInputCommand) { + if (!repository?.provider.acceptInputCommand) { return Promise.resolve(null); } + const id = repository.provider.acceptInputCommand.id; const args = repository.provider.acceptInputCommand.arguments; - const commandService = accessor.get(ICommandService); + return commandService.executeCommand(id, ...(args || [])); } }); const viewNextCommitCommand = { - description: { description: localize('scm view next commit', "SCM: View Next Commit"), args: [] }, + description: { description: localize('scm view next commit', "Source Control: View Next Commit"), args: [] }, weight: KeybindingWeight.WorkbenchContrib, handler: (accessor: ServicesAccessor) => { const contextKeyService = accessor.get(IContextKeyService); + const scmService = accessor.get(ISCMService); const context = contextKeyService.getContext(document.activeElement); - const repository = context.getValue<ISCMRepository>('scmRepository'); + const repositoryId = context.getValue<string | undefined>('scmRepository'); + const repository = repositoryId ? scmService.getRepository(repositoryId) : undefined; repository?.input.showNextHistoryValue(); } }; const viewPreviousCommitCommand = { - description: { description: localize('scm view previous commit', "SCM: View Previous Commit"), args: [] }, + description: { description: localize('scm view previous commit', "Source Control: View Previous Commit"), args: [] }, weight: KeybindingWeight.WorkbenchContrib, handler: (accessor: ServicesAccessor) => { const contextKeyService = accessor.get(IContextKeyService); + const scmService = accessor.get(ISCMService); const context = contextKeyService.getContext(document.activeElement); - const repository = context.getValue<ISCMRepository>('scmRepository'); + const repositoryId = context.getValue<string | undefined>('scmRepository'); + const repository = repositoryId ? scmService.getRepository(repositoryId) : undefined; repository?.input.showPreviousHistoryValue(); } }; diff --git a/src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts index e66a0f24fd5..3772fac9387 100644 --- a/src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts @@ -23,6 +23,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { RepositoryRenderer } from 'vs/workbench/contrib/scm/browser/scmRepositoryRenderer'; import { collectContextMenuActions, getActionViewItemProvider } from 'vs/workbench/contrib/scm/browser/util'; import { Orientation } from 'vs/base/browser/ui/sash/sash'; +import { Iterable } from 'vs/base/common/iterator'; class ListDelegate implements IListVirtualDelegate<ISCMRepository> { @@ -155,23 +156,28 @@ export class SCMRepositoriesViewPane extends ViewPane { } private updateListSelection(): void { - const set = new Set(); + const oldSelection = this.list.getSelection(); + const oldSet = new Set(Iterable.map(oldSelection, i => this.list.element(i))); + const set = new Set(this.scmViewService.visibleRepositories); + const added = new Set(Iterable.filter(set, r => !oldSet.has(r))); + const removed = new Set(Iterable.filter(oldSet, r => !set.has(r))); - for (const repository of this.scmViewService.visibleRepositories) { - set.add(repository); + if (added.size === 0 && removed.size === 0) { + return; } - const selection: number[] = []; + const selection = oldSelection + .filter(i => !removed.has(this.list.element(i))); for (let i = 0; i < this.list.length; i++) { - if (set.has(this.list.element(i))) { + if (added.has(this.list.element(i))) { selection.push(i); } } this.list.setSelection(selection); - if (selection.length > 0) { + if (selection.length > 0 && selection.indexOf(this.list.getFocus()[0]) === -1) { this.list.setAnchor(selection[0]); this.list.setFocus([selection[0]]); } diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 79cce93a380..7e87c10e0fc 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -10,7 +10,7 @@ import { IDisposable, Disposable, DisposableStore, combinedDisposable, dispose, import { ViewPane, IViewPaneOptions, ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { append, $, Dimension, asCSSUrl, trackFocus, clearNode } from 'vs/base/browser/dom'; import { IListVirtualDelegate, IIdentityProvider } from 'vs/base/browser/ui/list/list'; -import { ISCMResourceGroup, ISCMResource, InputValidationType, ISCMRepository, ISCMInput, IInputValidation, ISCMViewService, ISCMViewVisibleRepositoryChangeEvent, ISCMService, SCMInputChangeReason, VIEW_PANE_ID, ISCMActionButton, ISCMActionButtonDescriptor } from 'vs/workbench/contrib/scm/common/scm'; +import { ISCMResourceGroup, ISCMResource, InputValidationType, ISCMRepository, ISCMInput, IInputValidation, ISCMViewService, ISCMViewVisibleRepositoryChangeEvent, ISCMService, SCMInputChangeReason, VIEW_PANE_ID, ISCMActionButton, ISCMActionButtonDescriptor, ISCMRepositorySortKey, REPOSITORIES_VIEW_PANE_ID } from 'vs/workbench/contrib/scm/common/scm'; import { ResourceLabels, IResourceLabel, IFileLabelOptions } from 'vs/workbench/browser/labels'; import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -84,6 +84,7 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { MarkdownRenderer } from 'vs/editor/contrib/markdownRenderer/browser/markdownRenderer'; import { Button, ButtonWithDescription } from 'vs/base/browser/ui/button/button'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { RepositoryContextKeys } from 'vs/workbench/contrib/scm/browser/scmViewService'; type TreeElement = ISCMRepository | ISCMInput | ISCMActionButton | ISCMResourceGroup | IResourceNode<ISCMResource, ISCMResourceGroup> | ISCMResource; @@ -944,7 +945,7 @@ class RepositoryVisibilityAction extends Action2 { f1: false, precondition: ContextKeyExpr.or(ContextKeys.RepositoryVisibilityCount.notEqualsTo(1), ContextKeys.RepositoryVisibility(repository).isEqualTo(false)), toggled: ContextKeys.RepositoryVisibility(repository).isEqualTo(true), - menu: { id: Menus.Repositories } + menu: { id: Menus.Repositories, group: '0_repositories' } }); this.repository = repository; } @@ -1069,9 +1070,14 @@ class ViewModel { } } + // Update sort key based on view mode + this.sortKey = this.getViewModelSortKey(); + this.refresh(); this._onDidChangeMode.fire(mode); this.modeContextKey.set(mode); + + this.storageService.store(`scm.viewMode`, mode, StorageScope.WORKSPACE, StorageTarget.USER); } get sortKey(): ViewModelSortKey { return this._sortKey; } @@ -1085,6 +1091,10 @@ class ViewModel { this.refresh(); this._onDidChangeSortKey.fire(sortKey); this.sortKeyContextKey.set(sortKey); + + if (this._mode === ViewModelMode.List) { + this.storageService.store(`scm.viewSortKey`, sortKey, StorageScope.WORKSPACE, StorageTarget.USER); + } } private _treeViewStateIsStale = false; @@ -1113,23 +1123,37 @@ class ViewModel { private scmProviderRootUriContextKey: IContextKey<string | undefined>; private scmProviderHasRootUriContextKey: IContextKey<boolean>; + private _mode: ViewModelMode; + private _sortKey: ViewModelSortKey; + private _treeViewState: ITreeViewState | undefined; + constructor( private tree: WorkbenchCompressibleObjectTree<TreeElement, FuzzyScore>, private inputRenderer: InputRenderer, - private _mode: ViewModelMode, - private _sortKey: ViewModelSortKey, - private _treeViewState: ITreeViewState | undefined, @IInstantiationService protected instantiationService: IInstantiationService, @IEditorService protected editorService: IEditorService, @IConfigurationService protected configurationService: IConfigurationService, @ISCMViewService private scmViewService: ISCMViewService, + @IStorageService private storageService: IStorageService, @IUriIdentityService private uriIdentityService: IUriIdentityService, @IContextKeyService contextKeyService: IContextKeyService ) { + // View mode and sort key + this._mode = this.getViewModelMode(); + this._sortKey = this.getViewModelSortKey(); + + // TreeView state + const storageViewState = this.storageService.get(`scm.viewState`, StorageScope.WORKSPACE); + if (storageViewState) { + try { + this._treeViewState = JSON.parse(storageViewState); + } catch {/* noop */ } + } + this.modeContextKey = ContextKeys.ViewModelMode.bindTo(contextKeyService); - this.modeContextKey.set(_mode); + this.modeContextKey.set(this._mode); this.sortKeyContextKey = ContextKeys.ViewModelSortKey.bindTo(contextKeyService); - this.sortKeyContextKey.set(_sortKey); + this.sortKeyContextKey.set(this._sortKey); this.areAllRepositoriesCollapsedContextKey = ContextKeys.ViewModelAreAllRepositoriesCollapsed.bindTo(contextKeyService); this.isAnyRepositoryCollapsibleContextKey = ContextKeys.ViewModelIsAnyRepositoryCollapsible.bindTo(contextKeyService); this.scmProviderContextKey = ContextKeys.SCMProvider.bindTo(contextKeyService); @@ -1143,6 +1167,12 @@ class ViewModel { (this.updateRepositoryCollapseAllContextKeys, this, this.disposables); this.disposables.add(this.tree.onDidChangeCollapseState(() => this._treeViewStateIsStale = true)); + + this.storageService.onWillSaveState(e => { + if (e.reason === WillSaveStateReason.SHUTDOWN) { + this.storageService.store(`scm.viewState`, JSON.stringify(this.treeViewState), StorageScope.WORKSPACE, StorageTarget.MACHINE); + } + }); } private onDidChangeConfiguration(e?: IConfigurationChangeEvent): void { @@ -1432,6 +1462,45 @@ class ViewModel { } } + private getViewModelMode(): ViewModelMode { + let mode = this.configurationService.getValue<'tree' | 'list'>('scm.defaultViewMode') === 'list' ? ViewModelMode.List : ViewModelMode.Tree; + const storageMode = this.storageService.get(`scm.viewMode`, StorageScope.WORKSPACE) as ViewModelMode; + if (typeof storageMode === 'string') { + mode = storageMode; + } + + return mode; + } + + private getViewModelSortKey(): ViewModelSortKey { + // Tree + if (this._mode === ViewModelMode.Tree) { + return ViewModelSortKey.Path; + } + + // List + let viewSortKey: ViewModelSortKey; + const viewSortKeyString = this.configurationService.getValue<'path' | 'name' | 'status'>('scm.defaultViewSortKey'); + switch (viewSortKeyString) { + case 'name': + viewSortKey = ViewModelSortKey.Name; + break; + case 'status': + viewSortKey = ViewModelSortKey.Status; + break; + default: + viewSortKey = ViewModelSortKey.Path; + break; + } + + const storageSortKey = this.storageService.get(`scm.viewSortKey`, StorageScope.WORKSPACE) as ViewModelSortKey; + if (typeof storageSortKey === 'string') { + viewSortKey = storageSortKey; + } + + return viewSortKey; + } + dispose(): void { this.visibilityDisposables.dispose(); this.disposables.dispose(); @@ -1503,6 +1572,56 @@ registerAction2(SetTreeViewModeAction); registerAction2(SetListViewModeNavigationAction); registerAction2(SetTreeViewModeNavigationAction); +abstract class RepositorySortAction extends ViewAction<SCMViewPane> { + constructor(private sortKey: ISCMRepositorySortKey, title: string) { + super({ + id: `workbench.scm.action.repositories.setSortKey.${sortKey}`, + title, + viewId: VIEW_PANE_ID, + f1: false, + toggled: RepositoryContextKeys.RepositorySortKey.isEqualTo(sortKey), + menu: [ + { + id: Menus.Repositories, + group: '1_sort' + }, + { + id: MenuId.ViewTitle, + when: ContextKeyExpr.equals('view', REPOSITORIES_VIEW_PANE_ID), + group: '1_sort', + }, + ] + }); + } + + runInView(accessor: ServicesAccessor) { + accessor.get(ISCMViewService).toggleSortKey(this.sortKey); + } +} + + +class RepositorySortByDiscoveryTimeAction extends RepositorySortAction { + constructor() { + super(ISCMRepositorySortKey.DiscoveryTime, localize('repositorySortByDiscoveryTime', "Sort by Discovery Time")); + } +} + +class RepositorySortByNameAction extends RepositorySortAction { + constructor() { + super(ISCMRepositorySortKey.Name, localize('repositorySortByName', "Sort by Name")); + } +} + +class RepositorySortByPathAction extends RepositorySortAction { + constructor() { + super(ISCMRepositorySortKey.Path, localize('repositorySortByPath', "Sort by Path")); + } +} + +registerAction2(RepositorySortByDiscoveryTimeAction); +registerAction2(RepositorySortByNameAction); +registerAction2(RepositorySortByPathAction); + abstract class SetSortKeyAction extends ViewAction<SCMViewPane> { constructor(private sortKey: ViewModelSortKey, title: string) { super({ @@ -1511,6 +1630,7 @@ abstract class SetSortKeyAction extends ViewAction<SCMViewPane> { viewId: VIEW_PANE_ID, f1: false, toggled: ContextKeys.ViewModelSortKey.isEqualTo(sortKey), + precondition: ContextKeys.ViewModelMode.isEqualTo(ViewModelMode.List), menu: { id: Menus.ViewSort, group: '2_sort' } }); } @@ -1522,19 +1642,19 @@ abstract class SetSortKeyAction extends ViewAction<SCMViewPane> { class SetSortByNameAction extends SetSortKeyAction { constructor() { - super(ViewModelSortKey.Name, localize('sortByName', "Sort by Name")); + super(ViewModelSortKey.Name, localize('sortChangesByName', "Sort Changes by Name")); } } class SetSortByPathAction extends SetSortKeyAction { constructor() { - super(ViewModelSortKey.Path, localize('sortByPath', "Sort by Path")); + super(ViewModelSortKey.Path, localize('sortChangesByPath', "Sort Changes by Path")); } } class SetSortByStatusAction extends SetSortKeyAction { constructor() { - super(ViewModelSortKey.Status, localize('sortByStatus', "Sort by Status")); + super(ViewModelSortKey.Status, localize('sortChangesByStatus', "Sort Changes by Status")); } } @@ -1604,7 +1724,7 @@ class SCMInputWidget extends Disposable { private inputEditor: CodeEditorWidget; private model: { readonly input: ISCMInput; readonly textModel: ITextModel } | undefined; - private repositoryContextKey: IContextKey<ISCMRepository | undefined>; + private repositoryIdContextKey: IContextKey<string | undefined>; private repositoryDisposables = new DisposableStore(); private validation: IInputValidation | undefined; @@ -1633,7 +1753,7 @@ class SCMInputWidget extends Disposable { this.repositoryDisposables.dispose(); this.repositoryDisposables = new DisposableStore(); - this.repositoryContextKey.set(input?.repository); + this.repositoryIdContextKey.set(input?.repository.id); if (!input) { this.model?.textModel.dispose(); @@ -1799,7 +1919,7 @@ class SCMInputWidget extends Disposable { this.setPlaceholderFontStyles(fontFamily, fontSize, lineHeight); const contextKeyService2 = contextKeyService.createScoped(this.element); - this.repositoryContextKey = contextKeyService2.createKey('scmRepository', undefined); + this.repositoryIdContextKey = contextKeyService2.createKey('scmRepository', undefined); const editorOptions: IEditorConstructionOptions = { ...getSimpleEditorOptions(), @@ -2058,7 +2178,6 @@ export class SCMViewPane extends ViewPane { @IConfigurationService configurationService: IConfigurationService, @IContextKeyService contextKeyService: IContextKeyService, @IMenuService private menuService: IMenuService, - @IStorageService private storageService: IStorageService, @IOpenerService openerService: IOpenerService, @ITelemetryService telemetryService: ITelemetryService, ) { @@ -2145,44 +2264,9 @@ export class SCMViewPane extends ViewPane { append(this.listContainer, overflowWidgetsDomNode); - let viewMode = this.configurationService.getValue<'tree' | 'list'>('scm.defaultViewMode') === 'list' ? ViewModelMode.List : ViewModelMode.Tree; - - const storageMode = this.storageService.get(`scm.viewMode`, StorageScope.WORKSPACE) as ViewModelMode; - if (typeof storageMode === 'string') { - viewMode = storageMode; - } - - let viewSortKey: ViewModelSortKey; - const viewSortKeyString = this.configurationService.getValue<'path' | 'name' | 'status'>('scm.defaultViewSortKey'); - switch (viewSortKeyString) { - case 'name': - viewSortKey = ViewModelSortKey.Name; - break; - case 'status': - viewSortKey = ViewModelSortKey.Status; - break; - default: - viewSortKey = ViewModelSortKey.Path; - break; - } - - const storageSortKey = this.storageService.get(`scm.viewSortKey`, StorageScope.WORKSPACE) as ViewModelSortKey; - if (typeof storageSortKey === 'string') { - viewSortKey = storageSortKey; - } - - let viewState: ITreeViewState | undefined; - - const storageViewState = this.storageService.get(`scm.viewState`, StorageScope.WORKSPACE); - if (storageViewState) { - try { - viewState = JSON.parse(storageViewState); - } catch {/* noop */ } - } - this._register(this.instantiationService.createInstance(RepositoryVisibilityActionController)); - this._viewModel = this.instantiationService.createInstance(ViewModel, this.tree, this.inputRenderer, viewMode, viewSortKey, viewState); + this._viewModel = this.instantiationService.createInstance(ViewModel, this.tree, this.inputRenderer); this._register(this._viewModel); this.listContainer.classList.add('file-icon-themable-tree'); @@ -2191,18 +2275,11 @@ export class SCMViewPane extends ViewPane { this.updateIndentStyles(this.themeService.getFileIconTheme()); this._register(this.themeService.onDidFileIconThemeChange(this.updateIndentStyles, this)); this._register(this._viewModel.onDidChangeMode(this.onDidChangeMode, this)); - this._register(this._viewModel.onDidChangeSortKey(this.onDidChangeSortKey, this)); this._register(this.onDidChangeBodyVisibility(this._viewModel.setVisible, this._viewModel)); this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.alwaysShowRepositories'))(this.updateActions, this)); this.updateActions(); - - this._register(this.storageService.onWillSaveState(e => { - if (e.reason === WillSaveStateReason.SHUTDOWN) { - this.storageService.store(`scm.viewState`, JSON.stringify(this._viewModel.treeViewState), StorageScope.WORKSPACE, StorageTarget.MACHINE); - } - })); } private updateIndentStyles(theme: IFileIconTheme): void { @@ -2214,11 +2291,6 @@ export class SCMViewPane extends ViewPane { private onDidChangeMode(): void { this.updateIndentStyles(this.themeService.getFileIconTheme()); - this.storageService.store(`scm.viewMode`, this._viewModel.mode, StorageScope.WORKSPACE, StorageTarget.USER); - } - - private onDidChangeSortKey(): void { - this.storageService.store(`scm.viewSortKey`, this._viewModel.sortKey, StorageScope.WORKSPACE, StorageTarget.USER); } override layoutBody(height: number | undefined = this.layoutCache.height, width: number | undefined = this.layoutCache.width): void { @@ -2254,14 +2326,14 @@ export class SCMViewPane extends ViewPane { return; } else if (isSCMResourceGroup(e.element)) { const provider = e.element.provider; - const repository = this.scmService.repositories.find(r => r.provider === provider); + const repository = Iterable.find(this.scmService.repositories, r => r.provider === provider); if (repository) { this.scmViewService.focus(repository); } return; } else if (ResourceTree.isResourceNode(e.element)) { const provider = e.element.context.provider; - const repository = this.scmService.repositories.find(r => r.provider === provider); + const repository = Iterable.find(this.scmService.repositories, r => r.provider === provider); if (repository) { this.scmViewService.focus(repository); } @@ -2304,7 +2376,7 @@ export class SCMViewPane extends ViewPane { } const provider = e.element.resourceGroup.provider; - const repository = this.scmService.repositories.find(r => r.provider === provider); + const repository = Iterable.find(this.scmService.repositories, r => r.provider === provider); if (repository) { this.scmViewService.focus(repository); @@ -2379,7 +2451,11 @@ export class SCMViewPane extends ViewPane { } override shouldShowWelcome(): boolean { - return this.scmService.repositories.length === 0; + return this.scmService.repositoryCount === 0; + } + + override getActionsContext(): unknown { + return this.scmViewService.visibleRepositories.length === 1 ? this.scmViewService.visibleRepositories[0].provider : undefined; } } diff --git a/src/vs/workbench/contrib/scm/browser/scmViewService.ts b/src/vs/workbench/contrib/scm/browser/scmViewService.ts index 757c5526c7a..be0176c4919 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewService.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewService.ts @@ -5,7 +5,7 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { Emitter, Event } from 'vs/base/common/event'; -import { ISCMViewService, ISCMRepository, ISCMService, ISCMViewVisibleRepositoryChangeEvent, ISCMMenus, ISCMProvider } from 'vs/workbench/contrib/scm/common/scm'; +import { ISCMViewService, ISCMRepository, ISCMService, ISCMViewVisibleRepositoryChangeEvent, ISCMMenus, ISCMProvider, ISCMRepositorySortKey } from 'vs/workbench/contrib/scm/common/scm'; import { Iterable } from 'vs/base/common/iterator'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { SCMMenus } from 'vs/workbench/contrib/scm/browser/menus'; @@ -15,6 +15,8 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { compareFileNames, comparePaths } from 'vs/base/common/comparers'; import { basename } from 'vs/base/common/resources'; import { binarySearch } from 'vs/base/common/arrays'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; function getProviderStorageKey(provider: ISCMProvider): string { return `${provider.contextValue}:${provider.label}${provider.rootUri ? `:${provider.rootUri.toString()}` : ''}`; @@ -29,8 +31,20 @@ function getRepositoryName(workspaceContextService: IWorkspaceContextService, re return folder?.uri.toString() === repository.provider.rootUri.toString() ? folder.name : basename(repository.provider.rootUri); } +export const RepositoryContextKeys = { + RepositorySortKey: new RawContextKey<ISCMRepositorySortKey>('scmRepositorySortKey', ISCMRepositorySortKey.DiscoveryTime), +}; + +interface ISCMRepositoryView { + readonly repository: ISCMRepository; + readonly discoveryTime: number; + focused: boolean; + selectionIndex: number; +} + export interface ISCMViewServiceState { readonly all: string[]; + readonly sortKey: ISCMRepositorySortKey; readonly visible: number[]; } @@ -45,20 +59,24 @@ export class SCMViewService implements ISCMViewService { private previousState: ISCMViewServiceState | undefined; private disposables = new DisposableStore(); - private _repositories: ISCMRepository[] = []; + private _repositories: ISCMRepositoryView[] = []; get repositories(): ISCMRepository[] { - return this._repositories; + return this._repositories.map(r => r.repository); } - private _onDidChangeRepositories = new Emitter<ISCMViewVisibleRepositoryChangeEvent>(); - readonly onDidChangeRepositories = this._onDidChangeRepositories.event; - - private _visibleRepositoriesSet = new Set<ISCMRepository>(); - private _visibleRepositories: ISCMRepository[] = []; - get visibleRepositories(): ISCMRepository[] { - return this._visibleRepositories; + // In order to match the legacy behaviour, when the repositories are sorted by discovery time, + // the visible repositories are sorted by the selection index instead of the discovery time. + if (this._repositoriesSortKey === ISCMRepositorySortKey.DiscoveryTime) { + return this._repositories.filter(r => r.selectionIndex !== -1) + .sort((r1, r2) => r1.selectionIndex - r2.selectionIndex) + .map(r => r.repository); + } + + return this._repositories + .filter(r => r.selectionIndex !== -1) + .map(r => r.repository); } set visibleRepositories(visibleRepositories: ISCMRepository[]) { @@ -66,15 +84,18 @@ export class SCMViewService implements ISCMViewService { const added = new Set<ISCMRepository>(); const removed = new Set<ISCMRepository>(); - for (const repository of visibleRepositories) { - if (!this._visibleRepositoriesSet.has(repository)) { - added.add(repository); + for (const repositoryView of this._repositories) { + // Selected -> !Selected + if (!set.has(repositoryView.repository) && repositoryView.selectionIndex !== -1) { + repositoryView.selectionIndex = -1; + removed.add(repositoryView.repository); } - } - - for (const repository of this._visibleRepositories) { - if (!set.has(repository)) { - removed.add(repository); + // Selected | !Selected -> Selected + if (set.has(repositoryView.repository)) { + if (repositoryView.selectionIndex === -1) { + added.add(repositoryView.repository); + } + repositoryView.selectionIndex = visibleRepositories.indexOf(repositoryView.repository); } } @@ -82,15 +103,17 @@ export class SCMViewService implements ISCMViewService { return; } - this._visibleRepositories = visibleRepositories.sort(this._compareRepositories); - this._visibleRepositoriesSet = set; this._onDidSetVisibleRepositories.fire({ added, removed }); - if (this._focusedRepository && removed.has(this._focusedRepository)) { - this.focus(this._visibleRepositories[0]); + // Update focus if the focused repository is not visible anymore + if (this._repositories.find(r => r.focused && r.selectionIndex === -1)) { + this.focus(this._repositories.find(r => r.selectionIndex !== -1)?.repository); } } + private _onDidChangeRepositories = new Emitter<ISCMViewVisibleRepositoryChangeEvent>(); + readonly onDidChangeRepositories = this._onDidChangeRepositories.event; + private _onDidSetVisibleRepositories = new Emitter<ISCMViewVisibleRepositoryChangeEvent>(); readonly onDidChangeVisibleRepositories = Event.any( this._onDidSetVisibleRepositories.event, @@ -101,53 +124,61 @@ export class SCMViewService implements ISCMViewService { return e; } - return { - added: Iterable.concat(last.added, e.added), - removed: Iterable.concat(last.removed, e.removed), - }; + const added = new Set(last.added); + const removed = new Set(last.removed); + + for (const repository of e.added) { + if (removed.has(repository)) { + removed.delete(repository); + } else { + added.add(repository); + } + } + for (const repository of e.removed) { + if (added.has(repository)) { + added.delete(repository); + } else { + removed.add(repository); + } + } + + return { added, removed }; }, 0) ); - private _focusedRepository: ISCMRepository | undefined; - get focusedRepository(): ISCMRepository | undefined { - return this._focusedRepository; + return this._repositories.find(r => r.focused)?.repository; } private _onDidFocusRepository = new Emitter<ISCMRepository | undefined>(); readonly onDidFocusRepository = this._onDidFocusRepository.event; - private _compareRepositories: (op1: ISCMRepository, op2: ISCMRepository) => number; + private _repositoriesSortKey: ISCMRepositorySortKey; + private _sortKeyContextKey: IContextKey<ISCMRepositorySortKey>; constructor( - @ISCMService private readonly scmService: ISCMService, + @ISCMService scmService: ISCMService, + @IContextKeyService contextKeyService: IContextKeyService, @IInstantiationService instantiationService: IInstantiationService, + @IConfigurationService private readonly configurationService: IConfigurationService, @IStorageService private readonly storageService: IStorageService, - @IWorkspaceContextService workspaceContextService: IWorkspaceContextService + @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService ) { this.menus = instantiationService.createInstance(SCMMenus); - this._compareRepositories = (op1: ISCMRepository, op2: ISCMRepository): number => { - const name1 = getRepositoryName(workspaceContextService, op1); - const name2 = getRepositoryName(workspaceContextService, op2); - - const nameComparison = compareFileNames(name1, name2); - if (nameComparison === 0 && op1.provider.rootUri && op2.provider.rootUri) { - return comparePaths(op1.provider.rootUri.fsPath, op2.provider.rootUri.fsPath); - } - - return nameComparison; - }; - - scmService.onDidAddRepository(this.onDidAddRepository, this, this.disposables); - scmService.onDidRemoveRepository(this.onDidRemoveRepository, this, this.disposables); - try { this.previousState = JSON.parse(storageService.get('scm:view:visibleRepositories', StorageScope.WORKSPACE, '')); } catch { // noop } + this._repositoriesSortKey = this.previousState?.sortKey ?? this.getViewSortOrder(); + this._sortKeyContextKey = RepositoryContextKeys.RepositorySortKey.bindTo(contextKeyService); + this._sortKeyContextKey.set(this._repositoriesSortKey); + + scmService.onDidAddRepository(this.onDidAddRepository, this, this.disposables); + scmService.onDidRemoveRepository(this.onDidRemoveRepository, this, this.disposables); + for (const repository of scmService.repositories) { this.onDidAddRepository(repository); } @@ -160,50 +191,61 @@ export class SCMViewService implements ISCMViewService { this.eventuallyFinishLoading(); } - this.insertRepository(this._repositories, repository); + const repositoryView: ISCMRepositoryView = { + repository, discoveryTime: Date.now(), focused: false, selectionIndex: -1 + }; + let removed: Iterable<ISCMRepository> = Iterable.empty(); if (this.previousState) { const index = this.previousState.all.indexOf(getProviderStorageKey(repository.provider)); - if (index === -1) { // saw a repo we did not expect + if (index === -1) { + // This repository is not part of the previous state which means that it + // was either manually closed in the previous session, or the repository + // was added after the previous session.In this case, we should select all + // of the repositories. const added: ISCMRepository[] = []; - for (const repo of this.scmService.repositories) { // all should be visible - if (!this._visibleRepositoriesSet.has(repo)) { - added.push(repository); + + this.insertRepositoryView(this._repositories, repositoryView); + this._repositories.forEach((repositoryView, index) => { + if (repositoryView.selectionIndex === -1) { + added.push(repositoryView.repository); } - } + repositoryView.selectionIndex = index; + }); - this._visibleRepositoriesSet = new Set(this.scmService.repositories); - this._visibleRepositories = [...this.scmService.repositories.sort(this._compareRepositories)]; this._onDidChangeRepositories.fire({ added, removed: Iterable.empty() }); - this.finishLoading(); + this.didSelectRepository = false; return; } - if (this.previousState.visible.indexOf(index) > -1) { - // First visible repository - if (!this.didSelectRepository) { - removed = this._visibleRepositories; - - this._visibleRepositories = []; - this._visibleRepositoriesSet = new Set(); - this.didSelectRepository = true; - } - } else { + if (this.previousState.visible.indexOf(index) === -1) { // Explicit selection started if (this.didSelectRepository) { + this.insertRepositoryView(this._repositories, repositoryView); this._onDidChangeRepositories.fire({ added: Iterable.empty(), removed: Iterable.empty() }); return; } + } else { + // First visible repository + if (!this.didSelectRepository) { + removed = [...this.visibleRepositories]; + this._repositories.forEach(r => { + r.focused = false; + r.selectionIndex = -1; + }); + + this.didSelectRepository = true; + } } } - this._visibleRepositoriesSet.add(repository); - this.insertRepository(this._visibleRepositories, repository); - this._onDidChangeRepositories.fire({ added: [repository], removed }); + const maxSelectionIndex = this.getMaxSelectionIndex(); + this.insertRepositoryView(this._repositories, { ...repositoryView, selectionIndex: maxSelectionIndex + 1 }); + this._onDidChangeRepositories.fire({ added: [repositoryView.repository], removed }); - if (!this._focusedRepository) { + if (!this._repositories.find(r => r.focused)) { this.focus(repository); } } @@ -213,39 +255,29 @@ export class SCMViewService implements ISCMViewService { this.eventuallyFinishLoading(); } - let added: Iterable<ISCMRepository> = Iterable.empty(); + const repositoriesIndex = this._repositories.findIndex(r => r.repository === repository); - const repositoriesIndex = this._repositories.indexOf(repository); - const visibleRepositoriesIndex = this._visibleRepositories.indexOf(repository); - - if (repositoriesIndex > -1) { - this._repositories.splice(repositoriesIndex, 1); + if (repositoriesIndex === -1) { + return; } - if (visibleRepositoriesIndex > -1) { - this._visibleRepositories.splice(visibleRepositoriesIndex, 1); - this._visibleRepositoriesSet.delete(repository); - - if (this._repositories.length > 0 && this._visibleRepositories.length === 0) { - const first = this._repositories[0]; + let added: Iterable<ISCMRepository> = Iterable.empty(); + const repositoryView = this._repositories.splice(repositoriesIndex, 1); - this._visibleRepositories.push(first); - this._visibleRepositoriesSet.add(first); - added = [first]; - } + if (this._repositories.length > 0 && this.visibleRepositories.length === 0) { + this._repositories[0].selectionIndex = 0; + added = [this._repositories[0].repository]; } - if (repositoriesIndex > -1 || visibleRepositoriesIndex > -1) { - this._onDidChangeRepositories.fire({ added, removed: [repository] }); - } + this._onDidChangeRepositories.fire({ added, removed: repositoryView.map(r => r.repository) }); - if (this._focusedRepository === repository) { - this.focus(this._visibleRepositories[0]); + if (repositoryView.length === 1 && repositoryView[0].focused && this.visibleRepositories.length > 0) { + this.focus(this.visibleRepositories[0]); } } isVisible(repository: ISCMRepository): boolean { - return this._visibleRepositoriesSet.has(repository); + return this._repositories.find(r => r.repository === repository)?.selectionIndex !== -1; } toggleVisibility(repository: ISCMRepository, visible?: boolean): void { @@ -269,18 +301,71 @@ export class SCMViewService implements ISCMViewService { } } + toggleSortKey(sortKey: ISCMRepositorySortKey): void { + this._repositoriesSortKey = sortKey; + this._sortKeyContextKey.set(this._repositoriesSortKey); + this._repositories.sort(this.compareRepositories.bind(this)); + + this._onDidChangeRepositories.fire({ added: Iterable.empty(), removed: Iterable.empty() }); + } + focus(repository: ISCMRepository | undefined): void { - if (repository && !this.visibleRepositories.includes(repository)) { + if (repository && !this.isVisible(repository)) { return; } - this._focusedRepository = repository; - this._onDidFocusRepository.fire(repository); + this._repositories.forEach(r => r.focused = r.repository === repository); + + if (this._repositories.find(r => r.focused)) { + this._onDidFocusRepository.fire(repository); + } + } + + private compareRepositories(op1: ISCMRepositoryView, op2: ISCMRepositoryView): number { + // Sort by discovery time + if (this._repositoriesSortKey === ISCMRepositorySortKey.DiscoveryTime) { + return op1.discoveryTime - op2.discoveryTime; + } + + // Sort by path + if (this._repositoriesSortKey === 'path' && op1.repository.provider.rootUri && op2.repository.provider.rootUri) { + return comparePaths(op1.repository.provider.rootUri.fsPath, op2.repository.provider.rootUri.fsPath); + } + + // Sort by name, path + const name1 = getRepositoryName(this.workspaceContextService, op1.repository); + const name2 = getRepositoryName(this.workspaceContextService, op2.repository); + + const nameComparison = compareFileNames(name1, name2); + if (nameComparison === 0 && op1.repository.provider.rootUri && op2.repository.provider.rootUri) { + return comparePaths(op1.repository.provider.rootUri.fsPath, op2.repository.provider.rootUri.fsPath); + } + + return nameComparison; + } + + private getMaxSelectionIndex(): number { + return this._repositories.length === 0 ? -1 : + Math.max(...this._repositories.map(r => r.selectionIndex)); + } + + private getViewSortOrder(): ISCMRepositorySortKey { + const sortOder = this.configurationService.getValue<'discovery time' | 'name' | 'path'>('scm.repositories.sortOrder'); + switch (sortOder) { + case 'discovery time': + return ISCMRepositorySortKey.DiscoveryTime; + case 'name': + return ISCMRepositorySortKey.Name; + case 'path': + return ISCMRepositorySortKey.Path; + default: + return ISCMRepositorySortKey.DiscoveryTime; + } } - private insertRepository(repositories: ISCMRepository[], repository: ISCMRepository): void { - const index = binarySearch(repositories, repository, this._compareRepositories); - repositories.splice(index < 0 ? ~index : index, 0, repository); + private insertRepositoryView(repositories: ISCMRepositoryView[], repositoryView: ISCMRepositoryView): void { + const index = binarySearch(repositories, repositoryView, this.compareRepositories.bind(this)); + repositories.splice(index < 0 ? ~index : index, 0, repositoryView); } private onWillSaveState(): void { @@ -290,7 +375,7 @@ export class SCMViewService implements ISCMViewService { const all = this.repositories.map(r => getProviderStorageKey(r.provider)); const visible = this.visibleRepositories.map(r => all.indexOf(getProviderStorageKey(r.provider))); - const raw = JSON.stringify({ all, visible }); + const raw = JSON.stringify({ all, sortKey: this._repositoriesSortKey, visible }); this.storageService.store('scm:view:visibleRepositories', raw, StorageScope.WORKSPACE, StorageTarget.MACHINE); } diff --git a/src/vs/workbench/contrib/scm/common/scm.ts b/src/vs/workbench/contrib/scm/common/scm.ts index c46af5c54b5..e180e8e9d3c 100644 --- a/src/vs/workbench/contrib/scm/common/scm.ts +++ b/src/vs/workbench/contrib/scm/common/scm.ts @@ -135,6 +135,7 @@ export interface ISCMInput { } export interface ISCMRepository extends IDisposable { + readonly id: string; readonly provider: ISCMProvider; readonly input: ISCMInput; } @@ -144,9 +145,11 @@ export interface ISCMService { readonly _serviceBrand: undefined; readonly onDidAddRepository: Event<ISCMRepository>; readonly onDidRemoveRepository: Event<ISCMRepository>; - readonly repositories: ISCMRepository[]; + readonly repositories: Iterable<ISCMRepository>; + readonly repositoryCount: number; registerSCMProvider(provider: ISCMProvider): ISCMRepository; + getRepository(id: string): ISCMRepository | undefined; } export interface ISCMTitleMenu { @@ -168,6 +171,12 @@ export interface ISCMMenus { getRepositoryMenus(provider: ISCMProvider): ISCMRepositoryMenus; } +export const enum ISCMRepositorySortKey { + DiscoveryTime = 'discoveryTime', + Name = 'name', + Path = 'path' +} + export const ISCMViewService = createDecorator<ISCMViewService>('scmView'); export interface ISCMViewVisibleRepositoryChangeEvent { @@ -189,6 +198,8 @@ export interface ISCMViewService { isVisible(repository: ISCMRepository): boolean; toggleVisibility(repository: ISCMRepository, visible?: boolean): void; + toggleSortKey(sortKey: ISCMRepositorySortKey): void; + readonly focusedRepository: ISCMRepository | undefined; readonly onDidFocusRepository: Event<ISCMRepository | undefined>; focus(repository: ISCMRepository): void; diff --git a/src/vs/workbench/contrib/scm/common/scmService.ts b/src/vs/workbench/contrib/scm/common/scmService.ts index 8fbe71b55e8..7877ba1d34e 100644 --- a/src/vs/workbench/contrib/scm/common/scmService.ts +++ b/src/vs/workbench/contrib/scm/common/scmService.ts @@ -175,10 +175,10 @@ class SCMInput implements ISCMInput { const history = [...this.historyNavigator].map(s => s ?? ''); if (history.length === 0 || (history.length === 1 && history[0] === '')) { - return; + storageService.remove(key, StorageScope.GLOBAL); + } else { + storageService.store(key, JSON.stringify({ timestamp: new Date().getTime(), history }), StorageScope.GLOBAL, StorageTarget.MACHINE); } - - storageService.store(key, JSON.stringify({ timestamp: new Date().getTime(), history }), StorageScope.GLOBAL, StorageTarget.MACHINE); this.didChangeHistory = false; }); } @@ -242,6 +242,7 @@ class SCMRepository implements ISCMRepository { readonly input: ISCMInput = new SCMInput(this, this.storageService); constructor( + public readonly id: string, public readonly provider: ISCMProvider, private disposable: IDisposable, @IStorageService private storageService: IStorageService @@ -266,9 +267,9 @@ export class SCMService implements ISCMService { declare readonly _serviceBrand: undefined; - private _providerIds = new Set<string>(); - private _repositories: ISCMRepository[] = []; - get repositories(): ISCMRepository[] { return [...this._repositories]; } + private _repositories = new Map<string, ISCMRepository>(); + get repositories(): Iterable<ISCMRepository> { return this._repositories.values(); } + get repositoryCount(): number { return this._repositories.size; } private providerCount: IContextKey<number>; @@ -289,31 +290,25 @@ export class SCMService implements ISCMService { registerSCMProvider(provider: ISCMProvider): ISCMRepository { this.logService.trace('SCMService#registerSCMProvider'); - if (this._providerIds.has(provider.id)) { + if (this._repositories.has(provider.id)) { throw new Error(`SCM Provider ${provider.id} already exists.`); } - this._providerIds.add(provider.id); - const disposable = toDisposable(() => { - const index = this._repositories.indexOf(repository); - - if (index < 0) { - return; - } - - this._providerIds.delete(provider.id); - this._repositories.splice(index, 1); + this._repositories.delete(provider.id); this._onDidRemoveProvider.fire(repository); - - this.providerCount.set(this._repositories.length); + this.providerCount.set(this._repositories.size); }); - const repository = new SCMRepository(provider, disposable, this.storageService); - this._repositories.push(repository); + const repository = new SCMRepository(provider.id, provider, disposable, this.storageService); + this._repositories.set(provider.id, repository); this._onDidAddProvider.fire(repository); - this.providerCount.set(this._repositories.length); + this.providerCount.set(this._repositories.size); return repository; } + + getRepository(id: string): ISCMRepository | undefined { + return this._repositories.get(id); + } } diff --git a/src/vs/workbench/contrib/search/browser/search.contribution.ts b/src/vs/workbench/contrib/search/browser/search.contribution.ts index 4ed1c60a5ca..fb3d698f583 100644 --- a/src/vs/workbench/contrib/search/browser/search.contribution.ts +++ b/src/vs/workbench/contrib/search/browser/search.contribution.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Action } from 'vs/base/common/actions'; import { onUnexpectedError } from 'vs/base/common/errors'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import * as platform from 'vs/base/common/platform'; @@ -594,25 +593,37 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { when: ContextKeyExpr.and(ExplorerRootContext, ExplorerFolderContext.toNegated()) }); - -class ShowAllSymbolsAction extends Action { +registerAction2(class ShowAllSymbolsAction extends Action2 { static readonly ID = 'workbench.action.showAllSymbols'; static readonly LABEL = nls.localize('showTriggerActions', "Go to Symbol in Workspace..."); static readonly ALL_SYMBOLS_PREFIX = '#'; constructor( - actionId: string, - actionLabel: string, - @IQuickInputService private readonly quickInputService: IQuickInputService ) { - super(actionId, actionLabel); + super({ + id: 'workbench.action.showAllSymbols', + title: { + value: nls.localize('showTriggerActions', "Go to Symbol in Workspace..."), + original: 'Go to Symbol in Workspace...' + }, + f1: true, + menu: { + id: MenuId.TitleMenuQuickPick, + group: '1/workspaceNav', + order: 2 + }, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyCode.KeyT + } + }); } - override async run(): Promise<void> { - this.quickInputService.quickAccess.show(ShowAllSymbolsAction.ALL_SYMBOLS_PREFIX); + override async run(accessor: ServicesAccessor): Promise<void> { + accessor.get(IQuickInputService).quickAccess.show(ShowAllSymbolsAction.ALL_SYMBOLS_PREFIX); } -} +}); const SEARCH_MODE_CONFIG = 'search.mode'; @@ -793,7 +804,6 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } }); -registry.registerWorkbenchAction(SyncActionDescriptor.from(ShowAllSymbolsAction, { primary: KeyMod.CtrlCmd | KeyCode.KeyT }), 'Go to Symbol in Workspace...'); registry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleSearchOnTypeAction), 'Search: Toggle Search on Type', category.value); // Register Quick Access Handler diff --git a/src/vs/workbench/contrib/search/browser/searchResultsView.ts b/src/vs/workbench/contrib/search/browser/searchResultsView.ts index eebc83d22a0..976f351c60e 100644 --- a/src/vs/workbench/contrib/search/browser/searchResultsView.ts +++ b/src/vs/workbench/contrib/search/browser/searchResultsView.ts @@ -54,8 +54,10 @@ interface IMatchTemplate { export class SearchDelegate implements IListVirtualDelegate<RenderableMatch> { + public static ITEM_HEIGHT = 22; + getHeight(element: RenderableMatch): number { - return 22; + return SearchDelegate.ITEM_HEIGHT; } getTemplateId(element: RenderableMatch): string { diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index 90411d0caec..17baf536043 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -8,7 +8,6 @@ import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import * as aria from 'vs/base/browser/ui/aria/aria'; import { MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; import { IIdentityProvider } from 'vs/base/browser/ui/list/list'; -import { Orientation } from 'vs/base/browser/ui/sash/sash'; import { ITreeContextMenuEvent, ITreeElement } from 'vs/base/browser/ui/tree/tree'; import { IAction } from 'vs/base/common/actions'; import { Delayer } from 'vs/base/common/async'; @@ -45,20 +44,24 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { getSelectionKeyboardEvent, WorkbenchObjectTree } from 'vs/platform/list/browser/listService'; -import { INotificationService, } from 'vs/platform/notification/common/notification'; +import { INotificationService } from 'vs/platform/notification/common/notification'; import { IOpenerService, withSelection } from 'vs/platform/opener/common/opener'; import { IProgress, IProgressService, IProgressStep } from 'vs/platform/progress/common/progress'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { diffInserted, diffInsertedOutline, diffRemoved, diffRemovedOutline, editorFindMatchHighlight, editorFindMatchHighlightBorder, foreground, listActiveSelectionForeground, textLinkActiveForeground, textLinkForeground, toolbarActiveBackground, toolbarHoverBackground } from 'vs/platform/theme/common/colorRegistry'; +import { isHighContrast } from 'vs/platform/theme/common/theme'; import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { OpenFileFolderAction, OpenFolderAction } from 'vs/workbench/browser/actions/workspaceActions'; +import { ResourceListDnDHandler } from 'vs/workbench/browser/dnd'; import { ResourceLabels } from 'vs/workbench/browser/labels'; import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; import { IEditorPane } from 'vs/workbench/common/editor'; import { Memento, MementoObject } from 'vs/workbench/common/memento'; import { IViewDescriptorService } from 'vs/workbench/common/views'; +import { NotebookFindWidget } from 'vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget'; +import { NotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookEditor'; import { ExcludePatternInputWidget, IncludePatternInputWidget } from 'vs/workbench/contrib/search/browser/patternInputWidget'; import { appendKeyBindingLabel, IFindInFilesArgs } from 'vs/workbench/contrib/search/browser/searchActions'; import { searchDetailsIcon } from 'vs/workbench/contrib/search/browser/searchIcons'; @@ -66,7 +69,6 @@ import { renderSearchMessage } from 'vs/workbench/contrib/search/browser/searchM import { FileMatchRenderer, FolderMatchRenderer, MatchRenderer, SearchAccessibilityProvider, SearchDelegate } from 'vs/workbench/contrib/search/browser/searchResultsView'; import { ISearchWidgetOptions, SearchWidget } from 'vs/workbench/contrib/search/browser/searchWidget'; import * as Constants from 'vs/workbench/contrib/search/common/constants'; -import { ITextQueryBuilderOptions, QueryBuilder } from 'vs/workbench/services/search/common/queryBuilder'; import { IReplaceService } from 'vs/workbench/contrib/search/common/replace'; import { getOutOfWorkspaceEditorResources, SearchStateKey, SearchUIState } from 'vs/workbench/contrib/search/common/search'; import { ISearchHistoryService, ISearchHistoryValues } from 'vs/workbench/contrib/search/common/searchHistoryService'; @@ -74,11 +76,10 @@ import { FileMatch, FileMatchOrMatch, FolderMatch, FolderMatchWithResource, ICha import { createEditorFromSearchResult } from 'vs/workbench/contrib/searchEditor/browser/searchEditorActions'; import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IPreferencesService, ISettingsEditorOptions } from 'vs/workbench/services/preferences/common/preferences'; +import { ITextQueryBuilderOptions, QueryBuilder } from 'vs/workbench/services/search/common/queryBuilder'; import { IPatternInfo, ISearchComplete, ISearchConfiguration, ISearchConfigurationProperties, ITextQuery, SearchCompletionExitCode, SearchSortOrder, TextSearchCompleteMessageType } from 'vs/workbench/services/search/common/search'; import { TextSearchCompleteMessage } from 'vs/workbench/services/search/common/searchExtTypes'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { ResourceListDnDHandler } from 'vs/workbench/browser/dnd'; -import { isHighContrast } from 'vs/platform/theme/common/theme'; const $ = dom.$; @@ -739,7 +740,8 @@ export class SearchView extends ViewPane { selectionNavigation: true, overrideStyles: { listBackground: this.getBackgroundColor() - } + }, + additionalScrollHeight: SearchDelegate.ITEM_HEIGHT })); this._register(this.tree.onContextMenu(e => this.onContextMenu(e))); const updateHasSomeCollapsible = () => this.toggleCollapseStateDelayer.trigger(() => this.hasSomeCollapsibleResultKey.set(this.hasSomeCollapsible())); @@ -1065,7 +1067,9 @@ export class SearchView extends ViewPane { this.inputPatternExcludes.setWidth(this.size.width - 28 /* container margin */); this.inputPatternIncludes.setWidth(this.size.width - 28 /* container margin */); - this.tree.layout(); // The tree will measure its container + const widgetHeight = dom.getTotalHeight(this.searchWidgetsContainerElement); + const messagesHeight = dom.getTotalHeight(this.messagesElement); + this.tree.layout(this.size.height - widgetHeight - messagesHeight, this.size.width - 28); } protected override layoutBody(height: number, width: number): void { @@ -1282,7 +1286,7 @@ export class SearchView extends ViewPane { } if (!skipLayout && this.size) { - this.layout(this._orientation === Orientation.VERTICAL ? this.size.height : this.size.width); + this.reLayout(); } } @@ -1710,18 +1714,22 @@ export class SearchView extends ViewPane { this.open(lineMatch, preserveFocus, sideBySide, pinned); } - open(element: FileMatchOrMatch, preserveFocus?: boolean, sideBySide?: boolean, pinned?: boolean): Promise<void> { + async open(element: FileMatchOrMatch, preserveFocus?: boolean, sideBySide?: boolean, pinned?: boolean): Promise<void> { const selection = this.getSelectionFrom(element); const resource = element instanceof Match ? element.parent().resource : (<FileMatch>element).resource; - return this.editorService.openEditor({ - resource: resource, - options: { - preserveFocus, - pinned, - selection, - revealIfVisible: true - } - }, sideBySide ? SIDE_GROUP : ACTIVE_GROUP).then(editor => { + + let editor: IEditorPane | undefined; + try { + editor = await this.editorService.openEditor({ + resource: resource, + options: { + preserveFocus, + pinned, + selection, + revealIfVisible: true + } + }, sideBySide ? SIDE_GROUP : ACTIVE_GROUP); + const editorControl = editor?.getControl(); if (element instanceof Match && preserveFocus && isCodeEditor(editorControl)) { this.viewModel.searchResult.rangeHighlightDecorations.highlightRange( @@ -1731,7 +1739,16 @@ export class SearchView extends ViewPane { } else { this.viewModel.searchResult.rangeHighlightDecorations.removeHighlightRange(); } - }, errors.onUnexpectedError); + } catch (err) { + errors.onUnexpectedError(err); + return; + } + + if (editor instanceof NotebookEditor) { + const controller = editor.getControl()?.getContribution<NotebookFindWidget>(NotebookFindWidget.id); + const matchIndex = element instanceof Match ? element.parent().matches().findIndex(e => e.id() === element.id()) : undefined; + controller?.show(this.searchWidget.searchInput.getValue(), { matchIndex, focus: false }); + } } openEditorWithMultiCursor(element: FileMatchOrMatch): Promise<void> { diff --git a/src/vs/workbench/contrib/search/browser/searchWidget.ts b/src/vs/workbench/contrib/search/browser/searchWidget.ts index d9d0f908be9..aed2458e00b 100644 --- a/src/vs/workbench/contrib/search/browser/searchWidget.ts +++ b/src/vs/workbench/contrib/search/browser/searchWidget.ts @@ -366,7 +366,12 @@ export class SearchWidget extends Widget { this.contextLinesInput = new InputBox(searchInputContainer, this.contextViewService, { type: 'number' }); this.contextLinesInput.element.classList.add('context-lines-input'); this.contextLinesInput.value = '' + (this.configurationService.getValue<ISearchConfigurationProperties>('search').searchEditor.defaultNumberOfContextLines ?? 1); - this._register(this.contextLinesInput.onDidChange(() => this.onContextLinesChanged())); + this._register(this.contextLinesInput.onDidChange((value: string) => { + if (value !== '0') { + this.showContextToggle.checked = true; + } + this.onContextLinesChanged(); + })); this._register(attachInputBoxStyler(this.contextLinesInput, this.themeService)); dom.append(searchInputContainer, this.showContextToggle.domNode); } diff --git a/src/vs/workbench/contrib/search/test/electron-browser/textsearch.perf.integrationTest.ts b/src/vs/workbench/contrib/search/test/electron-browser/textsearch.perf.integrationTest.ts index d1a74339e79..cfcbec62d50 100644 --- a/src/vs/workbench/contrib/search/test/electron-browser/textsearch.perf.integrationTest.ts +++ b/src/vs/workbench/contrib/search/test/electron-browser/textsearch.perf.integrationTest.ts @@ -43,6 +43,7 @@ import { TestContextService, TestTextResourcePropertiesService } from 'vs/workbe import { TestEnvironmentService } from 'vs/workbench/test/electron-browser/workbenchTestServices'; import { LanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce'; import { LanguageFeaturesService } from 'vs/editor/common/services/languageFeaturesService'; +import { staticObservableValue } from 'vs/base/common/observableValue'; // declare var __dirname: string; @@ -182,7 +183,7 @@ suite.skip('TextSearch performance (integration)', () => { class TestTelemetryService implements ITelemetryService { public _serviceBrand: undefined; - public telemetryLevel = TelemetryLevel.USAGE; + public telemetryLevel = staticObservableValue(TelemetryLevel.USAGE); public sendErrorTelemetry = true; public events: any[] = []; diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts index 4d0742dc00a..ed23a5f2632 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts @@ -121,7 +121,7 @@ class SearchEditorInputSerializer implements IEditorSerializer { const config = input.tryReadConfigSync(); const dirty = input.isDirty(); - const matchRanges = input.getMatchRanges(); + const matchRanges = dirty ? input.getMatchRanges() : []; const backingUri = input.backingUri; return JSON.stringify({ modelUri, dirty, config, name: input.getName(), matchRanges, backingUri: backingUri?.toString() } as SerializedSearchEditor); diff --git a/src/vs/workbench/contrib/snippets/browser/configureSnippets.ts b/src/vs/workbench/contrib/snippets/browser/configureSnippets.ts index bbee0e94ef5..9b5871c2e89 100644 --- a/src/vs/workbench/contrib/snippets/browser/configureSnippets.ts +++ b/src/vs/workbench/contrib/snippets/browser/configureSnippets.ts @@ -4,11 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { extname } from 'vs/base/common/path'; -import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; +import { MenuId, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { URI } from 'vs/base/common/uri'; import { ISnippetsService } from 'vs/workbench/contrib/snippets/browser/snippets.contribution'; @@ -19,8 +18,7 @@ import { IFileService } from 'vs/platform/files/common/files'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { isValidBasename } from 'vs/base/common/extpath'; import { joinPath, basename } from 'vs/base/common/resources'; - -const id = 'workbench.action.openSnippets'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; namespace ISnippetPick { export function is(thing: object | undefined): thing is ISnippetPick { @@ -201,82 +199,80 @@ async function createLanguageSnippetFile(pick: ISnippetPick, fileService: IFileS await textFileService.write(pick.filepath, contents); } -CommandsRegistry.registerCommand(id, async (accessor): Promise<any> => { - - const snippetService = accessor.get(ISnippetsService); - const quickInputService = accessor.get(IQuickInputService); - const opener = accessor.get(IOpenerService); - const languageService = accessor.get(ILanguageService); - const envService = accessor.get(IEnvironmentService); - const workspaceService = accessor.get(IWorkspaceContextService); - const fileService = accessor.get(IFileService); - const textFileService = accessor.get(ITextFileService); - - const picks = await computePicks(snippetService, envService, languageService); - const existing: QuickPickInput[] = picks.existing; - - type SnippetPick = IQuickPickItem & { uri: URI } & { scope: string }; - const globalSnippetPicks: SnippetPick[] = [{ - scope: nls.localize('new.global_scope', 'global'), - label: nls.localize('new.global', "New Global Snippets file..."), - uri: envService.snippetsHome - }]; - - const workspaceSnippetPicks: SnippetPick[] = []; - for (const folder of workspaceService.getWorkspace().folders) { - workspaceSnippetPicks.push({ - scope: nls.localize('new.workspace_scope', "{0} workspace", folder.name), - label: nls.localize('new.folder', "New Snippets file for '{0}'...", folder.name), - uri: folder.toResource('.vscode') +registerAction2(class ConfigureSnippets extends Action2 { + + constructor() { + super({ + id: 'workbench.action.openSnippets', + title: { + value: nls.localize('openSnippet.label', "Configure User Snippets"), + original: 'Configure User Snippets' + }, + shortTitle: { + value: nls.localize('userSnippets', "User Snippets"), + mnemonicTitle: nls.localize({ key: 'miOpenSnippets', comment: ['&& denotes a mnemonic'] }, "User &&Snippets"), + original: 'User Snippets' + }, + menu: [ + { id: MenuId.CommandPalette }, + { id: MenuId.MenubarPreferencesMenu, group: '3_snippets', order: 1 }, + { id: MenuId.GlobalActivity, group: '3_snippets', order: 1 }, + ] }); } - if (existing.length > 0) { - existing.unshift({ type: 'separator', label: nls.localize('group.global', "Existing Snippets") }); - existing.push({ type: 'separator', label: nls.localize('new.global.sep', "New Snippets") }); - } else { - existing.push({ type: 'separator', label: nls.localize('new.global.sep', "New Snippets") }); - } - - const pick = await quickInputService.pick(([] as QuickPickInput[]).concat(existing, globalSnippetPicks, workspaceSnippetPicks, picks.future), { - placeHolder: nls.localize('openSnippet.pickLanguage', "Select Snippets File or Create Snippets"), - matchOnDescription: true - }); + async run(accessor: ServicesAccessor, ...args: any[]): Promise<any> { + + const snippetService = accessor.get(ISnippetsService); + const quickInputService = accessor.get(IQuickInputService); + const opener = accessor.get(IOpenerService); + const languageService = accessor.get(ILanguageService); + const envService = accessor.get(IEnvironmentService); + const workspaceService = accessor.get(IWorkspaceContextService); + const fileService = accessor.get(IFileService); + const textFileService = accessor.get(ITextFileService); + + const picks = await computePicks(snippetService, envService, languageService); + const existing: QuickPickInput[] = picks.existing; + + type SnippetPick = IQuickPickItem & { uri: URI } & { scope: string }; + const globalSnippetPicks: SnippetPick[] = [{ + scope: nls.localize('new.global_scope', 'global'), + label: nls.localize('new.global', "New Global Snippets file..."), + uri: envService.snippetsHome + }]; + + const workspaceSnippetPicks: SnippetPick[] = []; + for (const folder of workspaceService.getWorkspace().folders) { + workspaceSnippetPicks.push({ + scope: nls.localize('new.workspace_scope', "{0} workspace", folder.name), + label: nls.localize('new.folder', "New Snippets file for '{0}'...", folder.name), + uri: folder.toResource('.vscode') + }); + } - if (globalSnippetPicks.indexOf(pick as SnippetPick) >= 0) { - return createSnippetFile((pick as SnippetPick).scope, (pick as SnippetPick).uri, quickInputService, fileService, textFileService, opener); - } else if (workspaceSnippetPicks.indexOf(pick as SnippetPick) >= 0) { - return createSnippetFile((pick as SnippetPick).scope, (pick as SnippetPick).uri, quickInputService, fileService, textFileService, opener); - } else if (ISnippetPick.is(pick)) { - if (pick.hint) { - await createLanguageSnippetFile(pick, fileService, textFileService); + if (existing.length > 0) { + existing.unshift({ type: 'separator', label: nls.localize('group.global', "Existing Snippets") }); + existing.push({ type: 'separator', label: nls.localize('new.global.sep', "New Snippets") }); + } else { + existing.push({ type: 'separator', label: nls.localize('new.global.sep', "New Snippets") }); } - return opener.open(pick.filepath); - } -}); -MenuRegistry.appendMenuItem(MenuId.CommandPalette, { - command: { - id, - title: { value: nls.localize('openSnippet.label', "Configure User Snippets"), original: 'Configure User Snippets' }, - category: { value: nls.localize('preferences', "Preferences"), original: 'Preferences' } - } -}); + const pick = await quickInputService.pick(([] as QuickPickInput[]).concat(existing, globalSnippetPicks, workspaceSnippetPicks, picks.future), { + placeHolder: nls.localize('openSnippet.pickLanguage', "Select Snippets File or Create Snippets"), + matchOnDescription: true + }); -MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { - group: '3_snippets', - command: { - id, - title: nls.localize({ key: 'miOpenSnippets', comment: ['&& denotes a mnemonic'] }, "User &&Snippets") - }, - order: 1 -}); + if (globalSnippetPicks.indexOf(pick as SnippetPick) >= 0) { + return createSnippetFile((pick as SnippetPick).scope, (pick as SnippetPick).uri, quickInputService, fileService, textFileService, opener); + } else if (workspaceSnippetPicks.indexOf(pick as SnippetPick) >= 0) { + return createSnippetFile((pick as SnippetPick).scope, (pick as SnippetPick).uri, quickInputService, fileService, textFileService, opener); + } else if (ISnippetPick.is(pick)) { + if (pick.hint) { + await createLanguageSnippetFile(pick, fileService, textFileService); + } + return opener.open(pick.filepath); + } -MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { - group: '3_snippets', - command: { - id, - title: nls.localize('userSnippets', "User Snippets") - }, - order: 1 + } }); diff --git a/src/vs/workbench/contrib/snippets/browser/insertSnippet.ts b/src/vs/workbench/contrib/snippets/browser/insertSnippet.ts index 782cb1493fd..3a0088a1f48 100644 --- a/src/vs/workbench/contrib/snippets/browser/insertSnippet.ts +++ b/src/vs/workbench/contrib/snippets/browser/insertSnippet.ts @@ -112,7 +112,7 @@ class InsertSnippetAction extends EditorAction { } languageId = langId; } else { - editor.getModel().tokenizeIfCheap(lineNumber); + editor.getModel().tokenization.tokenizeIfCheap(lineNumber); languageId = editor.getModel().getLanguageIdAtPosition(lineNumber, column); // validate the `languageId` to ensure this is a user diff --git a/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts b/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts index aa6e96d9afe..2b7c3c8ed98 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts @@ -18,6 +18,7 @@ import { isPatternInWord } from 'vs/base/common/filters'; import { StopWatch } from 'vs/base/common/stopwatch'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { getWordAtText } from 'vs/editor/common/core/wordHelper'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; export class SnippetCompletion implements CompletionItem { @@ -29,6 +30,7 @@ export class SnippetCompletion implements CompletionItem { sortText: string; kind: CompletionItemKind; insertTextRules: CompletionItemInsertTextRule; + extensionId?: ExtensionIdentifier; constructor( readonly snippet: Snippet, @@ -37,6 +39,7 @@ export class SnippetCompletion implements CompletionItem { this.label = { label: snippet.prefix, description: snippet.name }; this.detail = localize('detail.snippet', "{0} ({1})", snippet.description || snippet.name, snippet.source); this.insertText = snippet.codeSnippet; + this.extensionId = snippet.extensionId; this.range = range; this.sortText = `${snippet.snippetSource === SnippetSource.Extension ? 'z' : 'a'}-${snippet.prefix}`; this.kind = CompletionItemKind.Snippet; @@ -182,7 +185,7 @@ export class SnippetCompletionProvider implements CompletionItemProvider { // validate the `languageId` to ensure this is a user // facing language with a name and the chance to have // snippets, else fall back to the outer language - model.tokenizeIfCheap(position.lineNumber); + model.tokenization.tokenizeIfCheap(position.lineNumber); let languageId = model.getLanguageIdAtPosition(position.lineNumber, position.column); if (!this._languageService.getLanguageName(languageId)) { languageId = model.getLanguageId(); diff --git a/src/vs/workbench/contrib/snippets/browser/snippetsFile.ts b/src/vs/workbench/contrib/snippets/browser/snippetsFile.ts index 25fafbfeda6..84f407b5f3e 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetsFile.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetsFile.ts @@ -12,7 +12,7 @@ import { KnownSnippetVariableNames } from 'vs/editor/contrib/snippet/browser/sni import { isFalsyOrWhitespace } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { IFileService } from 'vs/platform/files/common/files'; -import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { IdleValue } from 'vs/base/common/async'; import { IExtensionResourceLoaderService } from 'vs/workbench/services/extensionResourceLoader/common/extensionResourceLoader'; import { relativePath } from 'vs/base/common/resources'; @@ -114,7 +114,8 @@ export class Snippet { readonly body: string, readonly source: string, readonly snippetSource: SnippetSource, - readonly snippetIdentifier?: string + readonly snippetIdentifier?: string, + readonly extensionId?: ExtensionIdentifier, ) { this.prefixLow = prefix.toLowerCase(); this._bodyInsights = new IdleValue(() => new SnippetBodyInsights(this.body)); @@ -332,7 +333,8 @@ export class SnippetFile { body, source, this.source, - this._extension && `${relativePath(this._extension.extensionLocation, this.location)}/${name}` + this._extension && `${relativePath(this._extension.extensionLocation, this.location)}/${name}`, + this._extension?.identifier, )); } } diff --git a/src/vs/workbench/contrib/snippets/browser/surroundWithSnippet.ts b/src/vs/workbench/contrib/snippets/browser/surroundWithSnippet.ts index 96c1ecfc5a7..80d25b05968 100644 --- a/src/vs/workbench/contrib/snippets/browser/surroundWithSnippet.ts +++ b/src/vs/workbench/contrib/snippets/browser/surroundWithSnippet.ts @@ -38,7 +38,7 @@ registerAction2(class SurroundWithAction extends EditorAction2 { } const { lineNumber, column } = editor.getPosition(); - editor.getModel().tokenizeIfCheap(lineNumber); + editor.getModel().tokenization.tokenizeIfCheap(lineNumber); const languageId = editor.getModel().getLanguageIdAtPosition(lineNumber, column); const allSnippets = await snippetService.getSnippets(languageId, { includeNoPrefixSnippets: true, includeDisabledSnippets: true }); diff --git a/src/vs/workbench/contrib/snippets/browser/tabCompletion.ts b/src/vs/workbench/contrib/snippets/browser/tabCompletion.ts index 504da91fa71..1f4afaa0a94 100644 --- a/src/vs/workbench/contrib/snippets/browser/tabCompletion.ts +++ b/src/vs/workbench/contrib/snippets/browser/tabCompletion.ts @@ -91,7 +91,7 @@ export class TabCompletionController implements IEditorContribution { // lots of dance for getting the const selection = this._editor.getSelection(); const model = this._editor.getModel(); - model.tokenizeIfCheap(selection.positionLineNumber); + model.tokenization.tokenizeIfCheap(selection.positionLineNumber); const id = model.getLanguageIdAtPosition(selection.positionLineNumber, selection.positionColumn); const snippets = this._snippetService.getSnippetsSync(id); @@ -147,7 +147,7 @@ export class TabCompletionController implements IEditorContribution { } }; const registration = this._languageFeaturesService.completionProvider.register( - { language: model.getLanguageId(), pattern: model.uri.path, scheme: model.uri.scheme }, + { language: model.getLanguageId(), pattern: model.uri.fsPath, scheme: model.uri.scheme }, this._completionProvider ); } diff --git a/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts b/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts index 540e3fce155..9a602558814 100644 --- a/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts +++ b/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts @@ -6,15 +6,15 @@ import * as assert from 'assert'; import { SnippetCompletion, SnippetCompletionProvider } from 'vs/workbench/contrib/snippets/browser/snippetCompletionProvider'; import { Position } from 'vs/editor/common/core/position'; -import { ModesRegistry } from 'vs/editor/common/languages/modesRegistry'; -import { LanguageService } from 'vs/editor/common/services/languageService'; -import { createTextModel } from 'vs/editor/test/common/testTextModel'; +import { createModelServices, instantiateTextModel } from 'vs/editor/test/common/testTextModel'; import { ISnippetsService } from 'vs/workbench/contrib/snippets/browser/snippets.contribution'; import { Snippet, SnippetSource } from 'vs/workbench/contrib/snippets/browser/snippetsFile'; import { CompletionContext, CompletionItemLabel, CompletionItemRanges, CompletionTriggerKind } from 'vs/editor/common/languages'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; import { EditOperation } from 'vs/editor/common/core/editOperation'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { ILanguageService } from 'vs/editor/common/languages/language'; class SimpleSnippetService implements ISnippetsService { declare readonly _serviceBrand: undefined; @@ -37,27 +37,21 @@ class SimpleSnippetService implements ISnippetsService { } suite('SnippetsService', function () { - const disposableStore: DisposableStore = new DisposableStore(); const context: CompletionContext = { triggerKind: CompletionTriggerKind.Invoke }; - suiteSetup(function () { - disposableStore.add(ModesRegistry.registerLanguage({ - id: 'fooLang', - extensions: ['.fooLang',] - })); - }); - - suiteTeardown(function () { - disposableStore.dispose(); - }); - let disposables: DisposableStore; - let languageService: LanguageService; + let instantiationService: TestInstantiationService; + let languageService: ILanguageService; let snippetService: ISnippetsService; setup(function () { disposables = new DisposableStore(); - languageService = disposables.add(new LanguageService()); + instantiationService = createModelServices(disposables); + languageService = instantiationService.get(ILanguageService); + disposables.add(languageService.registerLanguage({ + id: 'fooLang', + extensions: ['.fooLang',] + })); snippetService = new SimpleSnippetService([new Snippet( ['fooLang'], 'barTest', @@ -84,7 +78,7 @@ suite('SnippetsService', function () { test('snippet completions - simple', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - const model = disposables.add(createTextModel('', 'fooLang')); + const model = disposables.add(instantiateTextModel(instantiationService, '', 'fooLang')); return provider.provideCompletionItems(model, new Position(1, 1), context)!.then(result => { assert.strictEqual(result.incomplete, undefined); @@ -95,7 +89,7 @@ suite('SnippetsService', function () { test('snippet completions - simple 2', async function () { const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - const model = disposables.add(createTextModel('hello ', 'fooLang')); + const model = disposables.add(instantiateTextModel(instantiationService, 'hello ', 'fooLang')); await provider.provideCompletionItems(model, new Position(1, 6) /* hello| */, context)!.then(result => { assert.strictEqual(result.incomplete, undefined); @@ -111,7 +105,7 @@ suite('SnippetsService', function () { test('snippet completions - with prefix', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - const model = disposables.add(createTextModel('bar', 'fooLang')); + const model = disposables.add(instantiateTextModel(instantiationService, 'bar', 'fooLang')); return provider.provideCompletionItems(model, new Position(1, 4), context)!.then(result => { assert.strictEqual(result.incomplete, undefined); @@ -146,7 +140,7 @@ suite('SnippetsService', function () { )]); const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - const model = disposables.add(createTextModel('bar-bar', 'fooLang')); + const model = disposables.add(instantiateTextModel(instantiationService, 'bar-bar', 'fooLang')); await provider.provideCompletionItems(model, new Position(1, 3), context)!.then(result => { assert.strictEqual(result.incomplete, undefined); @@ -217,19 +211,19 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = createTextModel('\t<?php', 'fooLang'); + let model = instantiateTextModel(instantiationService, '\t<?php', 'fooLang'); return provider.provideCompletionItems(model, new Position(1, 7), context)!.then(result => { assert.strictEqual(result.suggestions.length, 1); model.dispose(); - model = createTextModel('\t<?', 'fooLang'); + model = instantiateTextModel(instantiationService, '\t<?', 'fooLang'); return provider.provideCompletionItems(model, new Position(1, 4), context)!; }).then(result => { assert.strictEqual(result.suggestions.length, 1); assert.strictEqual((result.suggestions[0].range as any).insert.startColumn, 2); model.dispose(); - model = createTextModel('a<?', 'fooLang'); + model = instantiateTextModel(instantiationService, 'a<?', 'fooLang'); return provider.provideCompletionItems(model, new Position(1, 4), context)!; }).then(result => { assert.strictEqual(result.suggestions.length, 1); @@ -252,7 +246,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = disposables.add(createTextModel('<head>\n\t\n>/head>', 'fooLang')); + let model = disposables.add(instantiateTextModel(instantiationService, '<head>\n\t\n>/head>', 'fooLang')); return provider.provideCompletionItems(model, new Position(1, 1), context)!.then(result => { assert.strictEqual(result.suggestions.length, 1); return provider.provideCompletionItems(model, new Position(2, 2), context)!; @@ -282,7 +276,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = disposables.add(createTextModel('', 'fooLang')); + let model = disposables.add(instantiateTextModel(instantiationService, '', 'fooLang')); return provider.provideCompletionItems(model, new Position(1, 1), context)!.then(result => { assert.strictEqual(result.suggestions.length, 2); let [first, second] = result.suggestions; @@ -309,7 +303,7 @@ suite('SnippetsService', function () { )]); const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = disposables.add(createTextModel('p-', 'fooLang')); + let model = disposables.add(instantiateTextModel(instantiationService, 'p-', 'fooLang')); let result = await provider.provideCompletionItems(model, new Position(1, 2), context)!; assert.strictEqual(result.suggestions.length, 1); @@ -334,7 +328,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = disposables.add(createTextModel('Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea b', 'fooLang')); + let model = disposables.add(instantiateTextModel(instantiationService, 'Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea b', 'fooLang')); let result = await provider.provideCompletionItems(model, new Position(1, 158), context)!; assert.strictEqual(result.suggestions.length, 1); @@ -353,7 +347,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = disposables.add(createTextModel(':', 'fooLang')); + let model = disposables.add(instantiateTextModel(instantiationService, ':', 'fooLang')); let result = await provider.provideCompletionItems(model, new Position(1, 2), context)!; assert.strictEqual(result.suggestions.length, 0); @@ -372,7 +366,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = disposables.add(createTextModel('template', 'fooLang')); + let model = disposables.add(instantiateTextModel(instantiationService, 'template', 'fooLang')); let result = await provider.provideCompletionItems(model, new Position(1, 9), context)!; assert.strictEqual(result.suggestions.length, 1); @@ -395,7 +389,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = disposables.add(createTextModel('Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea b text_after_b', 'fooLang')); + let model = disposables.add(instantiateTextModel(instantiationService, 'Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea b text_after_b', 'fooLang')); let result = await provider.provideCompletionItems(model, new Position(1, 158), context)!; assert.strictEqual(result.suggestions.length, 1); @@ -403,7 +397,7 @@ suite('SnippetsService', function () { test('issue #61296: VS code freezes when editing CSS file with emoji', async function () { const languageConfigurationService = new TestLanguageConfigurationService(); - disposableStore.add(languageConfigurationService.register('fooLang', { + disposables.add(languageConfigurationService.register('fooLang', { wordPattern: /(#?-?\d*\.\d\w*%?)|(::?[\w-]*(?=[^,{;]*[,{]))|(([@#.!])?[\w-?]+%?|[@#!.])/g })); @@ -419,7 +413,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, languageConfigurationService); - let model = disposables.add(createTextModel('.🐷-a-b', 'fooLang')); + let model = disposables.add(instantiateTextModel(instantiationService, '.🐷-a-b', 'fooLang')); let result = await provider.provideCompletionItems(model, new Position(1, 8), context)!; assert.strictEqual(result.suggestions.length, 1); @@ -438,7 +432,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = disposables.add(createTextModel('a ', 'fooLang')); + let model = disposables.add(instantiateTextModel(instantiationService, 'a ', 'fooLang')); let result = await provider.provideCompletionItems(model, new Position(1, 3), context)!; assert.strictEqual(result.suggestions.length, 1); @@ -465,7 +459,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = createTextModel(' <', 'fooLang'); + let model = instantiateTextModel(instantiationService, ' <', 'fooLang'); let result = await provider.provideCompletionItems(model, new Position(1, 3), context)!; assert.strictEqual(result.suggestions.length, 1); @@ -473,7 +467,7 @@ suite('SnippetsService', function () { assert.strictEqual((first.range as any).insert.startColumn, 2); model.dispose(); - model = createTextModel('1', 'fooLang'); + model = instantiateTextModel(instantiationService, '1', 'fooLang'); result = await provider.provideCompletionItems(model, new Position(1, 2), context)!; assert.strictEqual(result.suggestions.length, 1); @@ -495,7 +489,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = createTextModel('not wordFoo bar', 'fooLang'); + let model = instantiateTextModel(instantiationService, 'not wordFoo bar', 'fooLang'); let result = await provider.provideCompletionItems(model, new Position(1, 3), context)!; assert.strictEqual(result.suggestions.length, 1); @@ -504,7 +498,7 @@ suite('SnippetsService', function () { assert.strictEqual((first.range as any).replace.endColumn, 9); model.dispose(); - model = createTextModel('not woFoo bar', 'fooLang'); + model = instantiateTextModel(instantiationService, 'not woFoo bar', 'fooLang'); result = await provider.provideCompletionItems(model, new Position(1, 3), context)!; assert.strictEqual(result.suggestions.length, 1); @@ -513,7 +507,7 @@ suite('SnippetsService', function () { assert.strictEqual((first.range as any).replace.endColumn, 3); model.dispose(); - model = createTextModel('not word', 'fooLang'); + model = instantiateTextModel(instantiationService, 'not word', 'fooLang'); result = await provider.provideCompletionItems(model, new Position(1, 1), context)!; assert.strictEqual(result.suggestions.length, 1); @@ -537,7 +531,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = createTextModel('filler e KEEP ng filler', 'fooLang'); + let model = instantiateTextModel(instantiationService, 'filler e KEEP ng filler', 'fooLang'); let result = await provider.provideCompletionItems(model, new Position(1, 9), context)!; assert.strictEqual(result.suggestions.length, 1); @@ -549,7 +543,7 @@ suite('SnippetsService', function () { test('Snippet will replace auto-closing pair if specified in prefix', async function () { const languageConfigurationService = new TestLanguageConfigurationService(); - disposableStore.add(languageConfigurationService.register('fooLang', { + disposables.add(languageConfigurationService.register('fooLang', { brackets: [ ['{', '}'], ['[', ']'], @@ -569,7 +563,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, languageConfigurationService); - let model = createTextModel('[psc]', 'fooLang'); + let model = instantiateTextModel(instantiationService, '[psc]', 'fooLang'); let result = await provider.provideCompletionItems(model, new Position(1, 5), context)!; assert.strictEqual(result.suggestions.length, 1); @@ -594,7 +588,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = createTextModel(' ci', 'fooLang'); + let model = instantiateTextModel(instantiationService, ' ci', 'fooLang'); let result = await provider.provideCompletionItems(model, new Position(1, 4), context)!; assert.strictEqual(result.suggestions.length, 1); @@ -615,7 +609,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = createTextModel('\'\'', 'fooLang'); + let model = instantiateTextModel(instantiationService, '\'\'', 'fooLang'); let result = await provider.provideCompletionItems( model, new Position(1, 2), @@ -637,7 +631,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = createTextModel('\'\'', 'fooLang'); + let model = instantiateTextModel(instantiationService, '\'\'', 'fooLang'); let result = await provider.provideCompletionItems( model, @@ -657,7 +651,7 @@ suite('SnippetsService', function () { ]); const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = createTextModel('\'hellot\'', 'fooLang'); + let model = instantiateTextModel(instantiationService, '\'hellot\'', 'fooLang'); let result = await provider.provideCompletionItems( model, @@ -678,7 +672,7 @@ suite('SnippetsService', function () { ]); const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = createTextModel(')*&^', 'fooLang'); + let model = instantiateTextModel(instantiationService, ')*&^', 'fooLang'); let result = await provider.provideCompletionItems( model, @@ -698,7 +692,7 @@ suite('SnippetsService', function () { ]); const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = createTextModel('foobar', 'fooLang'); + let model = instantiateTextModel(instantiationService, 'foobar', 'fooLang'); let result = await provider.provideCompletionItems( model, @@ -718,7 +712,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = createTextModel('function abc(w)', 'fooLang'); + let model = instantiateTextModel(instantiationService, 'function abc(w)', 'fooLang'); let result = await provider.provideCompletionItems( model, new Position(1, 15), @@ -737,7 +731,7 @@ suite('SnippetsService', function () { ]); const provider = new SnippetCompletionProvider(languageService, snippetService, new TestLanguageConfigurationService()); - let model = createTextModel('di', 'fooLang'); + let model = instantiateTextModel(instantiationService, 'di', 'fooLang'); let result = await provider.provideCompletionItems( model, new Position(1, 3), diff --git a/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTags.ts b/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTags.ts index 395145ee8b4..8e263a87401 100644 --- a/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTags.ts +++ b/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTags.ts @@ -36,7 +36,7 @@ export class WorkspaceTags implements IWorkbenchContribution { @IProductService private readonly productService: IProductService, @INativeHostService private readonly nativeHostService: INativeHostService ) { - if (this.telemetryService.telemetryLevel === TelemetryLevel.USAGE) { + if (this.telemetryService.telemetryLevel.value === TelemetryLevel.USAGE) { this.report(); } } diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index 70c269e4a69..5c133362dcb 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -16,6 +16,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import * as Types from 'vs/base/common/types'; import { TerminateResponseCode } from 'vs/base/common/processes'; import { ValidationStatus, ValidationState } from 'vs/base/common/parsers'; +import * as glob from 'vs/base/common/glob'; import * as UUID from 'vs/base/common/uuid'; import * as Platform from 'vs/base/common/platform'; import { LRUCache, Touch } from 'vs/base/common/map'; @@ -41,7 +42,7 @@ import { IConfigurationResolverService } from 'vs/workbench/services/configurati import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder, IWorkspace, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { IOutputService, IOutputChannel } from 'vs/workbench/contrib/output/common/output'; +import { IOutputService, IOutputChannel } from 'vs/workbench/services/output/common/output'; import { ITerminalGroupService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { ITerminalProfileResolverService } from 'vs/workbench/contrib/terminal/common/terminal'; @@ -69,7 +70,7 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/ import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { toFormattedString } from 'vs/base/common/jsonFormatter'; import { ITextModelService, IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService'; -import { SaveReason } from 'vs/workbench/common/editor'; +import { EditorResourceAccessor, SaveReason } from 'vs/workbench/common/editor'; import { ITextEditorSelection, TextEditorSelectionRevealType } from 'vs/platform/editor/common/editor'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; @@ -2753,11 +2754,21 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer }); } - private splitPerGroupType(tasks: Task[]): { none: Task[]; defaults: Task[] } { + /** + * + * @param tasks - The tasks which need filtering from defaults and non-defaults + * @param defaultType - If there are globs want globs in the default list, otherwise only tasks with true + * @param taskGlobsInList - This tells splitPerGroupType to filter out globbed tasks (into default), otherwise fall back to boolean + * @returns + */ + private splitPerGroupType(tasks: Task[], taskGlobsInList: boolean = false): { none: Task[]; defaults: Task[] } { let none: Task[] = []; let defaults: Task[] = []; for (let task of tasks) { - if ((task.configurationProperties.group as TaskGroup).isDefault) { + // At this point (assuming taskGlobsInList is true) there are tasks with matching globs, so only put those in defaults + if (taskGlobsInList && typeof (task.configurationProperties.group as TaskGroup).isDefault === 'string') { + defaults.push(task); + } else if (!taskGlobsInList && (task.configurationProperties.group as TaskGroup).isDefault === true) { defaults.push(task); } else { none.push(task); @@ -2766,54 +2777,38 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return { none, defaults }; } - private runBuildCommand(): void { + private runTaskGroupCommand(taskGroup: TaskGroup, strings: { + fetching: string; + select: string; + notFoundConfigure: string; + }, configure: () => void, legacyCommand: () => void): void { if (!this.canRunCommand()) { return; } if (this.schemaVersion === JsonSchemaVersion.V0_1_0) { - this.build(); + legacyCommand(); return; } let options: IProgressOptions = { location: ProgressLocation.Window, - title: nls.localize('TaskService.fetchingBuildTasks', 'Fetching build tasks...') + title: strings.fetching }; let promise = (async () => { - const buildTasks = await this._findWorkspaceTasksInGroup(TaskGroup.Build, false); - async function runSingleBuildTask(task: Task | undefined, problemMatcherOptions: ProblemMatcherRunOptions | undefined, that: AbstractTaskService) { + let taskGroupTasks: (Task | ConfiguringTask)[] = []; + + async function runSingleTask(task: Task | undefined, problemMatcherOptions: ProblemMatcherRunOptions | undefined, that: AbstractTaskService) { that.run(task, problemMatcherOptions, TaskRunSource.User).then(undefined, reason => { // eat the error, it has already been surfaced to the user and we don't care about it here }); } - if (buildTasks.length === 1) { - const buildTask = buildTasks[0]; - if (ConfiguringTask.is(buildTask)) { - return this.tryResolveTask(buildTask).then(resolvedTask => { - runSingleBuildTask(resolvedTask, undefined, this); - }); - } else { - runSingleBuildTask(buildTask, undefined, this); - return; - } - } - - return this.getTasksForGroup(TaskGroup.Build).then((tasks) => { - if (tasks.length > 0) { - let { none, defaults } = this.splitPerGroupType(tasks); - if (defaults.length === 1) { - runSingleBuildTask(defaults[0], undefined, this); - return; - } else if (defaults.length + none.length > 0) { - tasks = defaults.concat(none); - } - } + const chooseAndRunTask = (tasks: Task[]) => { this.showIgnoredFoldersMessage().then(() => { this.showQuickPick(tasks, - nls.localize('TaskService.pickBuildTask', 'Select the build task to run'), + strings.select, { - label: nls.localize('TaskService.noBuildTask', 'No build task to run found. Configure Build Task...'), + label: strings.notFoundConfigure, task: null }, true).then((entry) => { @@ -2822,66 +2817,104 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return; } if (task === null) { - this.runConfigureDefaultBuildTask(); + configure(); return; } - runSingleBuildTask(task, { attachProblemMatcher: true }, this); + runSingleTask(task, { attachProblemMatcher: true }, this); }); }); - }); - })(); - this.progressService.withProgress(options, () => promise); - } + }; - private runTestCommand(): void { - if (!this.canRunCommand()) { - return; - } - if (this.schemaVersion === JsonSchemaVersion.V0_1_0) { - this.runTest(); - return; - } - let options: IProgressOptions = { - location: ProgressLocation.Window, - title: nls.localize('TaskService.fetchingTestTasks', 'Fetching test tasks...') - }; - let promise = this.getTasksForGroup(TaskGroup.Test).then((tasks) => { - if (tasks.length > 0) { - let { none, defaults } = this.splitPerGroupType(tasks); - if (defaults.length === 1) { - this.run(defaults[0], undefined, TaskRunSource.User).then(undefined, reason => { - // eat the error, it has already been surfaced to the user and we don't care about it here - }); - return; - } else if (defaults.length + none.length > 0) { - tasks = defaults.concat(none); - } - } - this.showIgnoredFoldersMessage().then(() => { - this.showQuickPick(tasks, - nls.localize('TaskService.pickTestTask', 'Select the test task to run'), - { - label: nls.localize('TaskService.noTestTaskTerminal', 'No test task to run found. Configure Tasks...'), - task: null - }, true - ).then((entry) => { - let task: Task | undefined | null = entry ? entry.task : undefined; - if (task === undefined) { - return; + // First check for globs before checking for the default tasks of the task group + const absoluteURI = EditorResourceAccessor.getOriginalUri(this.editorService.activeEditor); + if (absoluteURI) { + const workspaceFolder = this.contextService.getWorkspaceFolder(absoluteURI); + // fallback to absolute path of the file if it is not in a workspace or relative path cannot be found + const relativePath = workspaceFolder?.uri ? (resources.relativePath(workspaceFolder.uri, absoluteURI) ?? absoluteURI.path) : absoluteURI.path; + + taskGroupTasks = await this._findWorkspaceTasks((task) => { + const taskGroup = task.configurationProperties.group; + if (taskGroup && typeof taskGroup !== 'string' && typeof taskGroup.isDefault === 'string') { + return (taskGroup._id === taskGroup._id && glob.match(taskGroup.isDefault, relativePath)); } - if (task === null) { - this.runConfigureTasks(); - return; + + return false; + }); + } + + const handleMultipleTasks = (areGlobTasks: boolean) => { + return this.getTasksForGroup(taskGroup).then((tasks) => { + if (tasks.length > 0) { + // If we're dealing with tasks that were chosen because of a glob match, + // then put globs in the defaults and everything else in none + let { none, defaults } = this.splitPerGroupType(tasks, areGlobTasks); + if (defaults.length === 1) { + runSingleTask(defaults[0], undefined, this); + return; + } else if (defaults.length + none.length > 0) { + tasks = defaults.concat(none); + } } - this.run(task, undefined, TaskRunSource.User).then(undefined, reason => { - // eat the error, it has already been surfaced to the user and we don't care about it here - }); + + // At this this point there are multiple tasks. + chooseAndRunTask(tasks); }); - }); - }); + }; + + const resolveTaskAndRun = (taskGroupTask: Task | ConfiguringTask) => { + if (ConfiguringTask.is(taskGroupTask)) { + this.tryResolveTask(taskGroupTask).then(resolvedTask => { + runSingleTask(resolvedTask, undefined, this); + }); + } else { + runSingleTask(taskGroupTask, undefined, this); + } + }; + + // A single default glob task was returned, just run it directly + if (taskGroupTasks.length === 1) { + return resolveTaskAndRun(taskGroupTasks[0]); + } + + // If there's multiple globs that match we want to show the quick picker for those tasks + // We will need to call splitPerGroupType putting globs in defaults and the remaining tasks in none. + // We don't need to carry on after here + if (taskGroupTasks.length > 1) { + return handleMultipleTasks(true); + } + + // If no globs are found or matched fallback to checking for default tasks of the task group + if (!taskGroupTasks.length) { + taskGroupTasks = await this._findWorkspaceTasksInGroup(taskGroup, false); + } + + // A single default task was returned, just run it directly + if (taskGroupTasks.length === 1) { + return resolveTaskAndRun(taskGroupTasks[0]); + } + + // Multiple default tasks returned, show the quickPicker + return handleMultipleTasks(false); + })(); this.progressService.withProgress(options, () => promise); } + private runBuildCommand(): void { + return this.runTaskGroupCommand(TaskGroup.Build, { + fetching: nls.localize('TaskService.fetchingBuildTasks', 'Fetching build tasks...'), + select: nls.localize('TaskService.pickBuildTask', 'Select the build task to run'), + notFoundConfigure: nls.localize('TaskService.noBuildTask', 'No build task to run found. Configure Build Task...') + }, this.runConfigureDefaultBuildTask, this.build); + } + + private runTestCommand(): void { + return this.runTaskGroupCommand(TaskGroup.Test, { + fetching: nls.localize('TaskService.fetchingTestTasks', 'Fetching test tasks...'), + select: nls.localize('TaskService.pickTestTask', 'Select the test task to run'), + notFoundConfigure: nls.localize('TaskService.noTestTaskTerminal', 'No test task to run found. Configure Tasks...') + }, this.runConfigureDefaultTestTask, this.runTest); + } + private runTerminateCommand(arg?: any): void { if (!this.canRunCommand()) { return; diff --git a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts index c0547a2ae39..992ecb05aec 100644 --- a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts +++ b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts @@ -29,7 +29,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { ITerminalProfileResolverService, TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal'; import { ITerminalService, ITerminalInstance, ITerminalGroupService } from 'vs/workbench/contrib/terminal/browser/terminal'; -import { IOutputService } from 'vs/workbench/contrib/output/common/output'; +import { IOutputService } from 'vs/workbench/services/output/common/output'; import { StartStopProblemCollector, WatchingProblemCollector, ProblemCollectorEventKind, ProblemHandlingStrategy } from 'vs/workbench/contrib/tasks/common/problemCollectors'; import { Task, CustomTask, ContributedTask, RevealKind, CommandOptions, ShellConfiguration, RuntimeType, PanelKind, diff --git a/src/vs/workbench/contrib/tasks/common/jsonSchema_v2.ts b/src/vs/workbench/contrib/tasks/common/jsonSchema_v2.ts index c3d81ff6df0..f0ba5e9fa63 100644 --- a/src/vs/workbench/contrib/tasks/common/jsonSchema_v2.ts +++ b/src/vs/workbench/contrib/tasks/common/jsonSchema_v2.ts @@ -192,9 +192,9 @@ const group: IJSONSchema = { properties: { kind: groupStrings, isDefault: { - type: 'boolean', + type: ['boolean', 'string'], default: false, - description: nls.localize('JsonSchema.tasks.group.isDefault', 'Defines if this task is the default task in the group.') + description: nls.localize('JsonSchema.tasks.group.isDefault', 'Defines if this task is the default task in the group, or a glob to match the file which should trigger this task.') } } }, diff --git a/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts b/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts index 7ab00229e89..9c6f0ba9e34 100644 --- a/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts +++ b/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts @@ -282,7 +282,7 @@ export interface CommandProperties extends BaseCommandProperties { export interface GroupKind { kind?: string; - isDefault?: boolean; + isDefault?: boolean | string; } export interface ConfigurationProperties { @@ -1245,7 +1245,7 @@ export namespace GroupKind { return { _id: external, isDefault: false }; } else if (Types.isString(external.kind) && Tasks.TaskGroup.is(external.kind)) { let group: string = external.kind; - let isDefault: boolean = !!external.isDefault; + let isDefault: boolean | string = Types.isUndefined(external.isDefault) ? false : external.isDefault; return { _id: group, isDefault }; } @@ -1260,7 +1260,7 @@ export namespace GroupKind { } return { kind: group._id, - isDefault: group.isDefault + isDefault: group.isDefault, }; } } diff --git a/src/vs/workbench/contrib/tasks/common/tasks.ts b/src/vs/workbench/contrib/tasks/common/tasks.ts index cb475cae0ee..1e7e42b6c81 100644 --- a/src/vs/workbench/contrib/tasks/common/tasks.ts +++ b/src/vs/workbench/contrib/tasks/common/tasks.ts @@ -393,7 +393,7 @@ export namespace TaskGroup { export interface TaskGroup { _id: string; - isDefault?: boolean; + isDefault?: boolean | string; } export const enum TaskScope { diff --git a/src/vs/workbench/contrib/tasks/electron-sandbox/taskService.ts b/src/vs/workbench/contrib/tasks/electron-sandbox/taskService.ts index 436f0194e19..a17d4185861 100644 --- a/src/vs/workbench/contrib/tasks/electron-sandbox/taskService.ts +++ b/src/vs/workbench/contrib/tasks/electron-sandbox/taskService.ts @@ -30,7 +30,7 @@ import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IViewsService, IViewDescriptorService } from 'vs/workbench/common/views'; -import { IOutputService } from 'vs/workbench/contrib/output/common/output'; +import { IOutputService } from 'vs/workbench/services/output/common/output'; import { ITerminalGroupService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; diff --git a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh index 5b7471bb284..0c5d62458d5 100755 --- a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh +++ b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh @@ -10,6 +10,10 @@ if [ -z "$VSCODE_SHELL_LOGIN" ]; then else # Imitate -l because --init-file doesn't support it: # run the first of these files that exists + if [ -f /etc/profile ]; then + . /etc/profile + fi + # exceute the first that exists if [ -f ~/.bash_profile ]; then . ~/.bash_profile elif [ -f ~/.bash_login ]; then @@ -21,48 +25,48 @@ else fi if [[ "$PROMPT_COMMAND" =~ .*(' '.*\;)|(\;.*' ').* ]]; then - echo -e "\033[1;33mShell integration cannot be activated due to complex PROMPT_COMMAND: $PROMPT_COMMAND\033[0m" + builtin echo -e "\033[1;33mShell integration cannot be activated due to complex PROMPT_COMMAND: $PROMPT_COMMAND\033[0m" VSCODE_SHELL_HIDE_WELCOME="" - return; + builtin return fi if [ -z "$VSCODE_SHELL_INTEGRATION" ]; then - return + builtin return fi __vsc_in_command_execution="1" __vsc_last_history_id=$(history 1 | awk '{print $1;}') __vsc_prompt_start() { - printf "\033]633;A\007" + builtin printf "\033]633;A\007" } __vsc_prompt_end() { - printf "\033]633;B\007" + builtin printf "\033]633;B\007" } __vsc_update_cwd() { - printf "\033]633;P;Cwd=%s\007" "$PWD" + builtin printf "\033]633;P;Cwd=%s\007" "$PWD" } __vsc_command_output_start() { - printf "\033]633;C\007" + builtin printf "\033]633;C\007" } __vsc_continuation_start() { - printf "\033]633;F\007" + builtin printf "\033]633;F\007" } __vsc_continuation_end() { - printf "\033]633;G\007" + builtin printf "\033]633;G\007" } __vsc_command_complete() { - local __vsc_history_id=$(history 1 | awk '{print $1;}') + local __vsc_history_id=$(builtin history 1 | awk '{print $1;}') if [[ "$__vsc_history_id" == "$__vsc_last_history_id" ]]; then - printf "\033]633;D\007" + builtin printf "\033]633;D\007" else - printf "\033]633;D;%s\007" "$__vsc_status" + builtin printf "\033]633;D;%s\007" "$__vsc_status" __vsc_last_history_id=$__vsc_history_id fi __vsc_update_cwd @@ -95,17 +99,26 @@ __vsc_preexec() { __vsc_update_prompt __vsc_prompt_cmd_original() { + if [[ ${IFS+set} ]]; then + __vsc_original_ifs="$IFS" + fi __vsc_status="$?" if [[ "$__vsc_original_prompt_command" =~ .+\;.+ ]]; then IFS=';' else IFS=' ' fi - read -ra ADDR <<<"$__vsc_original_prompt_command" + builtin read -ra ADDR <<<"$__vsc_original_prompt_command" + if [[ ${__vsc_original_ifs+set} ]]; then + IFS="$__vsc_original_ifs" + unset __vsc_original_ifs + else + unset IFS + fi for ((i = 0; i < ${#ADDR[@]}; i++)); do - eval ${ADDR[i]} + # unset IFS + builtin eval ${ADDR[i]} done - IFS='' __vsc_precmd } @@ -130,7 +143,7 @@ fi trap '__vsc_preexec' DEBUG if [ -z "$VSCODE_SHELL_HIDE_WELCOME" ]; then - echo -e "\033[1;32mShell integration activated\033[0m" + builtin echo -e "\033[1;32mShell integration activated\033[0m" else VSCODE_SHELL_HIDE_WELCOME="" fi diff --git a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-env.zsh b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-env.zsh new file mode 100644 index 00000000000..f676a0308bd --- /dev/null +++ b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-env.zsh @@ -0,0 +1,8 @@ +# --------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# --------------------------------------------------------------------------------------------- + +if [[ -f ~/.zshenv ]]; then + . ~/.zshenv +fi diff --git a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-profile.zsh b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-profile.zsh new file mode 100644 index 00000000000..734bb831e11 --- /dev/null +++ b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-profile.zsh @@ -0,0 +1,8 @@ +# --------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# --------------------------------------------------------------------------------------------- + +if [[ $options[norcs] = off && -o "login" && -f ~/.zprofile ]]; then + . ~/.zprofile +fi diff --git a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.zsh b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.zsh index be84dc9674f..2dec005daf4 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.zsh +++ b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.zsh @@ -2,73 +2,68 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. # --------------------------------------------------------------------------------------------- -autoload -Uz add-zsh-hook +builtin autoload -Uz add-zsh-hook # Now that the init script is running, unset ZDOTDIR to ensure ~/.zlogout runs as expected as well # as prevent problems that may occur if the user's init scripts depend on ZDOTDIR not being set. -unset ZDOTDIR +builtin unset ZDOTDIR # This variable allows the shell to both detect that VS Code's shell integration is enabled as well # as disable it by unsetting the variable. VSCODE_SHELL_INTEGRATION=1 -if [ -f ~/.zshenv ]; then - . ~/.zshenv -fi -if [[ -o "login" && -f ~/.zprofile ]]; then - . ~/.zprofile -fi -if [ -f ~/.zshrc ]; then + +if [[ $options[norcs] = off && -f ~/.zshrc ]]; then . ~/.zshrc fi # Shell integration was disabled by the shell, exit without warning assuming either the shell has # explicitly disabled shell integration as it's incompatible or it implements the protocol. if [ -z "$VSCODE_SHELL_INTEGRATION" ]; then - return + builtin return fi __vsc_in_command_execution="1" __vsc_last_history_id=0 __vsc_prompt_start() { - printf "\033]633;A\007" + builtin printf "\033]633;A\007" } __vsc_prompt_end() { - printf "\033]633;B\007" + builtin printf "\033]633;B\007" } __vsc_update_cwd() { - printf "\033]633;P;Cwd=%s\007" "$PWD" + builtin printf "\033]633;P;Cwd=%s\007" "$PWD" } __vsc_command_output_start() { - printf "\033]633;C\007" + builtin printf "\033]633;C\007" } __vsc_continuation_start() { - printf "\033]633;F\007" + builtin printf "\033]633;F\007" } __vsc_continuation_end() { - printf "\033]633;G\007" + builtin printf "\033]633;G\007" } __vsc_right_prompt_start() { - printf "\033]633;H\007" + builtin printf "\033]633;H\007" } __vsc_right_prompt_end() { - printf "\033]633;I\007" + builtin printf "\033]633;I\007" } __vsc_command_complete() { - local __vsc_history_id=$(history | tail -n1 | awk '{print $1;}') + builtin local __vsc_history_id=$(builtin history | tail -n1 | awk '{print $1;}') if [[ "$__vsc_history_id" == "$__vsc_last_history_id" ]]; then - printf "\033]633;D\007" + builtin printf "\033]633;D\007" else - printf "\033]633;D;%s\007" "$__vsc_status" + builtin printf "\033]633;D;%s\007" "$__vsc_status" __vsc_last_history_id=$__vsc_history_id fi __vsc_update_cwd @@ -114,7 +109,7 @@ add-zsh-hook preexec __vsc_preexec # Show the welcome message if [ -z "${VSCODE_SHELL_HIDE_WELCOME-}" ]; then - echo "\033[1;32mShell integration activated\033[0m" + builtin echo "\033[1;32mShell integration activated\033[0m" else VSCODE_SHELL_HIDE_WELCOME="" fi diff --git a/src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts b/src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts index 6168afff93b..4ba93e51fb3 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts @@ -10,6 +10,9 @@ import { FindReplaceState } from 'vs/editor/contrib/find/browser/findState'; import { ITerminalGroupService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; import { TerminalLocation } from 'vs/platform/terminal/common/terminal'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; export class TerminalFindWidget extends SimpleFindWidget { protected _findInputFocused: IContextKey<boolean>; @@ -19,11 +22,14 @@ export class TerminalFindWidget extends SimpleFindWidget { constructor( findState: FindReplaceState, @IContextViewService _contextViewService: IContextViewService, + @IKeybindingService keybindingService: IKeybindingService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @ITerminalService private readonly _terminalService: ITerminalService, - @ITerminalGroupService private readonly _terminalGroupService: ITerminalGroupService + @ITerminalGroupService private readonly _terminalGroupService: ITerminalGroupService, + @IThemeService private readonly _themeService: IThemeService, + @IConfigurationService private readonly _configurationService: IConfigurationService ) { - super(_contextViewService, _contextKeyService, findState, { showOptionButtons: true, showResultCount: true }); + super(findState, { showOptionButtons: true, showResultCount: true, type: 'Terminal' }, _contextViewService, _contextKeyService, keybindingService); this._register(findState.onFindReplaceStateChange(() => { this.show(); @@ -31,15 +37,25 @@ export class TerminalFindWidget extends SimpleFindWidget { this._findInputFocused = TerminalContextKeys.findInputFocus.bindTo(this._contextKeyService); this._findWidgetFocused = TerminalContextKeys.findFocus.bindTo(this._contextKeyService); this._findWidgetVisible = TerminalContextKeys.findVisible.bindTo(_contextKeyService); + this._register(this._themeService.onDidColorThemeChange(() => { + if (this._findWidgetVisible) { + this.find(true, true); + } + })); + this._register(this._configurationService.onDidChangeConfiguration((e) => { + if (e.affectsConfiguration('workbench.colorCustomizations') && this._findWidgetVisible) { + this.find(true, true); + } + })); } - find(previous: boolean) { + find(previous: boolean, update?: boolean) { const instance = this._terminalService.activeInstance; if (!instance) { return; } if (previous) { - instance.xterm?.findPrevious(this.inputValue, { regex: this._getRegexValue(), wholeWord: this._getWholeWordValue(), caseSensitive: this._getCaseSensitiveValue() }); + instance.xterm?.findPrevious(this.inputValue, { regex: this._getRegexValue(), wholeWord: this._getWholeWordValue(), caseSensitive: this._getCaseSensitiveValue(), incremental: update }); } else { instance.xterm?.findNext(this.inputValue, { regex: this._getRegexValue(), wholeWord: this._getWholeWordValue(), caseSensitive: this._getCaseSensitiveValue() }); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 18be1c0ae4a..9414adfc86a 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -460,6 +460,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // Re-establish the title after reconnect if (this.shellLaunchConfig.attachPersistentProcess) { this.refreshTabLabels(this.shellLaunchConfig.attachPersistentProcess.title, this.shellLaunchConfig.attachPersistentProcess.titleSource); + this.setShellType(this.shellType); } if (this._fixedCols) { @@ -1096,21 +1097,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { setTimeout(() => this._refreshSelectionContextKey(), 0); })); - this._register(dom.addDisposableListener(xterm.raw.textarea, 'focus', () => { - this._terminalFocusContextKey.set(true); - if (this.shellType) { - this._terminalShellTypeContextKey.set(this.shellType.toString()); - } else { - this._terminalShellTypeContextKey.reset(); - } - this._onDidFocus.fire(this); - })); - - this._register(dom.addDisposableListener(xterm.raw.textarea, 'blur', () => { - this._terminalFocusContextKey.reset(); - this._onDidBlur.fire(this); - this._refreshSelectionContextKey(); - })); + this._register(dom.addDisposableListener(xterm.raw.textarea, 'focus', () => this._setFocus(true))); + this._register(dom.addDisposableListener(xterm.raw.textarea, 'blur', () => this._setFocus(false))); + this._register(dom.addDisposableListener(xterm.raw.textarea, 'focusout', () => this._setFocus(false))); this._initDragAndDrop(container); @@ -1136,6 +1125,17 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } } + private _setFocus(focused?: boolean): void { + if (focused) { + this._terminalFocusContextKey.set(true); + this._onDidFocus.fire(this); + } else { + this._terminalFocusContextKey.reset(); + this._onDidBlur.fire(this); + this._refreshSelectionContextKey(); + } + } + private _initDragAndDrop(container: HTMLElement) { this._dndObserver?.dispose(); const dndController = this._instantiationService.createInstance(TerminalInstanceDragAndDropController, container); @@ -1157,6 +1157,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { if (asHtml) { const textAsHtml = await xterm.getSelectionAsHtml(command); function listener(e: any) { + if (!e.clipboardData.types.includes('text/plain')) { + e.clipboardData.setData('text/plain', command?.getOutput() ?? ''); + } e.clipboardData.setData('text/html', textAsHtml); e.preventDefault(); } @@ -1882,6 +1885,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { setShellType(shellType: TerminalShellType) { this._shellType = shellType; + if (shellType) { + this._terminalShellTypeContextKey.set(shellType?.toString()); + } } private _setAriaLabel(xterm: XTermTerminal | undefined, terminalId: number, title: string | undefined): void { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts index 5e749dcac5b..6c09333437c 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts @@ -388,7 +388,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce baseEnv = await this._terminalProfileResolverService.getEnvironment(this.remoteAuthority); } - const env = terminalEnvironment.createTerminalEnvironment(shellLaunchConfig, envFromConfigValue, variableResolver, this._productService.version, this._configHelper.config.detectLocale, baseEnv); + const env = await terminalEnvironment.createTerminalEnvironment(shellLaunchConfig, envFromConfigValue, variableResolver, this._productService.version, this._configHelper.config.detectLocale, baseEnv); if (!this._isDisposed && !shellLaunchConfig.strictEnv && !shellLaunchConfig.hideFromUser) { this._extEnvironmentVariableCollection = this._environmentVariableService.mergedCollection; this._register(this._environmentVariableService.onDidChangeCollections(newCollection => this._onEnvironmentVariableCollectionChange(newCollection))); @@ -398,7 +398,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce // info widget. While technically these could differ due to the slight change of a race // condition, the chance is minimal plus the impact on the user is also not that great // if it happens - it's not worth adding plumbing to sync back the resolved collection. - this._extEnvironmentVariableCollection.applyToProcessEnvironment(env, variableResolver); + await this._extEnvironmentVariableCollection.applyToProcessEnvironment(env, variableResolver); if (this._extEnvironmentVariableCollection.map.size > 0) { this.environmentVariableInfo = new EnvironmentVariableInfoChangesActive(this._extEnvironmentVariableCollection); this._onEnvironmentVariableInfoChange.fire(this.environmentVariableInfo); @@ -423,7 +423,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce const activeWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot(Schemas.file); - const initialCwd = terminalEnvironment.getCwd( + const initialCwd = await terminalEnvironment.getCwd( shellLaunchConfig, userHome, variableResolver, diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts b/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts index 5885b71b3a3..0849f6676b2 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts @@ -355,25 +355,23 @@ export abstract class BaseTerminalProfileResolverService implements ITerminalPro const env = await this._context.getEnvironment(options.remoteAuthority); const activeWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot(options.remoteAuthority ? Schemas.vscodeRemote : Schemas.file); const lastActiveWorkspace = activeWorkspaceRootUri ? withNullAsUndefined(this._workspaceContextService.getWorkspaceFolder(activeWorkspaceRootUri)) : undefined; - profile.path = this._resolveVariables(profile.path, env, lastActiveWorkspace); + profile.path = await this._resolveVariables(profile.path, env, lastActiveWorkspace); // Resolve args variables if (profile.args) { if (typeof profile.args === 'string') { - profile.args = this._resolveVariables(profile.args, env, lastActiveWorkspace); + profile.args = await this._resolveVariables(profile.args, env, lastActiveWorkspace); } else { - for (let i = 0; i < profile.args.length; i++) { - profile.args[i] = this._resolveVariables(profile.args[i], env, lastActiveWorkspace); - } + profile.args = await Promise.all(profile.args.map(arg => this._resolveVariables(arg, env, lastActiveWorkspace))); } } return profile; } - private _resolveVariables(value: string, env: IProcessEnvironment, lastActiveWorkspace: IWorkspaceFolder | undefined) { + private async _resolveVariables(value: string, env: IProcessEnvironment, lastActiveWorkspace: IWorkspaceFolder | undefined) { try { - value = this._configurationResolverService.resolveWithEnvironment(env, lastActiveWorkspace, value); + value = await this._configurationResolverService.resolveWithEnvironment(env, lastActiveWorkspace, value); } catch (e) { this._logService.error(`Could not resolve shell`, e); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index 8402e0e5362..62d52e82cc9 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -52,6 +52,7 @@ export class TerminalService implements ITerminalService { private _hostActiveTerminals: Map<ITerminalInstanceHost, ITerminalInstance | undefined> = new Map(); private _terminalEditorActive: IContextKey<boolean>; + private readonly _terminalShellTypeContextKey: IContextKey<string>; private _escapeSequenceLoggingEnabled: boolean = false; @@ -187,9 +188,15 @@ export class TerminalService implements ITerminalService { if (!instance && !this._isShuttingDown) { this._terminalGroupService.hidePanel(); } + if (instance?.shellType) { + this._terminalShellTypeContextKey.set(instance.shellType.toString()); + } else if (!instance) { + this._terminalShellTypeContextKey.reset(); + } }); this._handleInstanceContextKeys(); + this._terminalShellTypeContextKey = TerminalContextKeys.shellType.bindTo(this._contextKeyService); this._processSupportContextKey = TerminalContextKeys.processSupported.bindTo(this._contextKeyService); this._processSupportContextKey.set(!isWeb || this._remoteAgentService.getConnection() !== null); this._terminalHasBeenCreated = TerminalContextKeys.terminalHasBeenCreated.bindTo(this._contextKeyService); diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/commandNavigationAddon.ts b/src/vs/workbench/contrib/terminal/browser/xterm/commandNavigationAddon.ts index c7b911c633d..60d82f8436d 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/commandNavigationAddon.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/commandNavigationAddon.ts @@ -34,6 +34,9 @@ export class CommandNavigationAddon extends Disposable implements ICommandTracke activate(terminal: Terminal): void { this._terminal = terminal; + this._terminal.onData(() => { + this._currentMarker = Boundary.Bottom; + }); } constructor( diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts b/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts index 40b75ff1352..3f07b624fa7 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts @@ -73,6 +73,8 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { this._refreshStyles(); } else if (e.affectsConfiguration(TerminalSettingId.FontSize) || e.affectsConfiguration(TerminalSettingId.LineHeight)) { this.refreshLayouts(); + } else if (e.affectsConfiguration('workbench.colorCustomizations')) { + this._refreshStyles(true); } }); this._themeService.onDidColorThemeChange(() => this._refreshStyles(true)); @@ -94,10 +96,10 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { } else { color = ''; } - if (decoration.decoration.overviewRulerOptions) { - decoration.decoration.overviewRulerOptions.color = color; - } else { - decoration.decoration.overviewRulerOptions = { color }; + if (decoration.decoration.options?.overviewRulerOptions) { + decoration.decoration.options.overviewRulerOptions.color = color; + } else if (decoration.decoration.options) { + decoration.decoration.options.overviewRulerOptions = { color }; } } } diff --git a/src/vs/workbench/contrib/terminal/common/environmentVariable.ts b/src/vs/workbench/contrib/terminal/common/environmentVariable.ts index c307e138dd4..1f105bfaa94 100644 --- a/src/vs/workbench/contrib/terminal/common/environmentVariable.ts +++ b/src/vs/workbench/contrib/terminal/common/environmentVariable.ts @@ -7,6 +7,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { Event } from 'vs/base/common/event'; import { IProcessEnvironment } from 'vs/base/common/platform'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { VariableResolver } from 'vs/workbench/contrib/terminal/common/terminalEnvironment'; export const IEnvironmentVariableService = createDecorator<IEnvironmentVariableService>('environmentVariableService'); @@ -51,7 +52,7 @@ export interface IMergedEnvironmentVariableCollection { * @param variableResolver An optional function to use to resolve variables within the * environment values. */ - applyToProcessEnvironment(env: IProcessEnvironment, variableResolver?: (str: string) => string): void; + applyToProcessEnvironment(env: IProcessEnvironment, variableResolver?: VariableResolver): Promise<void>; /** * Generates a diff of this connection against another. Returns undefined if the collections are diff --git a/src/vs/workbench/contrib/terminal/common/environmentVariableCollection.ts b/src/vs/workbench/contrib/terminal/common/environmentVariableCollection.ts index a64ef609d29..e67f607c506 100644 --- a/src/vs/workbench/contrib/terminal/common/environmentVariableCollection.ts +++ b/src/vs/workbench/contrib/terminal/common/environmentVariableCollection.ts @@ -5,6 +5,7 @@ import { IProcessEnvironment, isWindows } from 'vs/base/common/platform'; import { EnvironmentVariableMutatorType, IEnvironmentVariableCollection, IExtensionOwnedEnvironmentVariableMutator, IMergedEnvironmentVariableCollection, IMergedEnvironmentVariableCollectionDiff } from 'vs/workbench/contrib/terminal/common/environmentVariable'; +import { VariableResolver } from 'vs/workbench/contrib/terminal/common/terminalEnvironment'; export class MergedEnvironmentVariableCollection implements IMergedEnvironmentVariableCollection { readonly map: Map<string, IExtensionOwnedEnvironmentVariableMutator[]> = new Map(); @@ -41,16 +42,16 @@ export class MergedEnvironmentVariableCollection implements IMergedEnvironmentVa }); } - applyToProcessEnvironment(env: IProcessEnvironment, variableResolver?: (str: string) => string): void { + async applyToProcessEnvironment(env: IProcessEnvironment, variableResolver?: VariableResolver): Promise<void> { let lowerToActualVariableNames: { [lowerKey: string]: string | undefined } | undefined; if (isWindows) { lowerToActualVariableNames = {}; Object.keys(env).forEach(e => lowerToActualVariableNames![e.toLowerCase()] = e); } - this.map.forEach((mutators, variable) => { + for (const [variable, mutators] of this.map) { const actualVariable = isWindows ? lowerToActualVariableNames![variable.toLowerCase()] || variable : variable; - mutators.forEach(mutator => { - const value = variableResolver ? variableResolver(mutator.value) : mutator.value; + for (const mutator of mutators) { + const value = variableResolver ? await variableResolver(mutator.value) : mutator.value; switch (mutator.type) { case EnvironmentVariableMutatorType.Append: env[actualVariable] = (env[actualVariable] || '') + value; @@ -62,8 +63,8 @@ export class MergedEnvironmentVariableCollection implements IMergedEnvironmentVa env[actualVariable] = value; break; } - }); - }); + } + } } diff(other: IMergedEnvironmentVariableCollection): IMergedEnvironmentVariableCollectionDiff | undefined { diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index 4020b3e6fa0..3ddae516bb0 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -186,7 +186,7 @@ const terminalConfiguration: IConfigurationNode = { default: DEFAULT_LINE_HEIGHT }, [TerminalSettingId.MinimumContrastRatio]: { - markdownDescription: localize('terminal.integrated.minimumContrastRatio', "When set the foreground color of each cell will change to try meet the contrast ratio specified. Example values:\n\n- 1: Do nothing and use the standard theme colors.\n- 4.5: [WCAG AA compliance (minimum)](https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html) (default).\n- 7: [WCAG AAA compliance (enhanced)](https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast7.html).\n- 21: White on black or black on white."), + markdownDescription: localize('terminal.integrated.minimumContrastRatio', "When set, the foreground color of each cell will change to try meet the contrast ratio specified. Note that this will not apply to `powerline` characters per #146406. Example values:\n\n- 1: Do nothing and use the standard theme colors.\n- 4.5: [WCAG AA compliance (minimum)](https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html) (default).\n- 7: [WCAG AAA compliance (enhanced)](https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast7.html).\n- 21: White on black or black on white."), type: 'number', default: 4.5 }, diff --git a/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts b/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts index b1922c5cd52..80041aeb869 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts @@ -78,17 +78,17 @@ function mergeNonNullKeys(env: IProcessEnvironment, other: ITerminalEnvironment } } -function resolveConfigurationVariables(variableResolver: VariableResolver, env: ITerminalEnvironment): ITerminalEnvironment { - Object.keys(env).forEach((key) => { - const value = env[key]; +async function resolveConfigurationVariables(variableResolver: VariableResolver, env: ITerminalEnvironment): Promise<ITerminalEnvironment> { + await Promise.all(Object.entries(env).map(async ([key, value]) => { if (typeof value === 'string') { try { - env[key] = variableResolver(value); + env[key] = await variableResolver(value); } catch (e) { env[key] = value; } } - }); + })); + return env; } @@ -179,17 +179,17 @@ export function getLangEnvVariable(locale?: string): string { return parts.join('_') + '.UTF-8'; } -export function getCwd( +export async function getCwd( shell: IShellLaunchConfig, userHome: string | undefined, variableResolver: VariableResolver | undefined, root: Uri | undefined, customCwd: string | undefined, logService?: ILogService -): string { +): Promise<string> { if (shell.cwd) { const unresolved = (typeof shell.cwd === 'object') ? shell.cwd.fsPath : shell.cwd; - const resolved = _resolveCwd(unresolved, variableResolver); + const resolved = await _resolveCwd(unresolved, variableResolver); return _sanitizeCwd(resolved || unresolved); } @@ -197,7 +197,7 @@ export function getCwd( if (!shell.ignoreConfigurationCwd && customCwd) { if (variableResolver) { - customCwd = _resolveCwd(customCwd, variableResolver, logService); + customCwd = await _resolveCwd(customCwd, variableResolver, logService); } if (customCwd) { if (path.isAbsolute(customCwd)) { @@ -216,10 +216,10 @@ export function getCwd( return _sanitizeCwd(cwd); } -function _resolveCwd(cwd: string, variableResolver: VariableResolver | undefined, logService?: ILogService): string | undefined { +async function _resolveCwd(cwd: string, variableResolver: VariableResolver | undefined, logService?: ILogService): Promise<string | undefined> { if (variableResolver) { try { - return variableResolver(cwd); + return await variableResolver(cwd); } catch (e) { logService?.error('Could not resolve terminal cwd', e); return undefined; @@ -251,7 +251,7 @@ export type TerminalShellArgsSetting = ( | TerminalSettingId.ShellArgsLinux ); -export type VariableResolver = (str: string) => string; +export type VariableResolver = (str: string) => Promise<string>; export function createVariableResolver(lastActiveWorkspace: IWorkspaceFolder | undefined, env: IProcessEnvironment, configurationResolverService: IConfigurationResolverService | undefined): VariableResolver | undefined { if (!configurationResolverService) { @@ -263,7 +263,7 @@ export function createVariableResolver(lastActiveWorkspace: IWorkspaceFolder | u /** * @deprecated Use ITerminalProfileResolverService */ -export function getDefaultShell( +export async function getDefaultShell( fetchSetting: (key: TerminalShellSetting) => string | undefined, defaultShell: string, isWoW64: boolean, @@ -272,7 +272,7 @@ export function getDefaultShell( logService: ILogService, useAutomationShell: boolean, platformOverride: Platform = platform -): string { +): Promise<string> { let maybeExecutable: string | undefined; if (useAutomationShell) { // If automationShell is specified, this should override the normal setting @@ -300,7 +300,7 @@ export function getDefaultShell( if (variableResolver) { try { - executable = variableResolver(executable); + executable = await variableResolver(executable); } catch (e) { logService.error(`Could not resolve shell`, e); } @@ -312,13 +312,13 @@ export function getDefaultShell( /** * @deprecated Use ITerminalProfileResolverService */ -export function getDefaultShellArgs( +export async function getDefaultShellArgs( fetchSetting: (key: TerminalShellSetting | TerminalShellArgsSetting) => string | string[] | undefined, useAutomationShell: boolean, variableResolver: VariableResolver | undefined, logService: ILogService, platformOverride: Platform = platform, -): string | string[] { +): Promise<string | string[]> { if (useAutomationShell) { if (!!getShellSetting(fetchSetting, 'automationShell', platformOverride)) { return []; @@ -331,13 +331,13 @@ export function getDefaultShellArgs( return []; } if (typeof args === 'string' && platformOverride === Platform.Windows) { - return variableResolver ? variableResolver(args) : args; + return variableResolver ? await variableResolver(args) : args; } if (variableResolver) { const resolvedArgs: string[] = []; for (const arg of args) { try { - resolvedArgs.push(variableResolver(arg)); + resolvedArgs.push(await variableResolver(arg)); } catch (e) { logService.error(`Could not resolve ${TerminalSettingPrefix.ShellArgs}${platformKey}`, e); resolvedArgs.push(arg); @@ -357,14 +357,14 @@ function getShellSetting( return fetchSetting(<TerminalShellSetting>`terminal.integrated.${type}.${platformKey}`); } -export function createTerminalEnvironment( +export async function createTerminalEnvironment( shellLaunchConfig: IShellLaunchConfig, envFromConfig: ITerminalEnvironment | undefined, variableResolver: VariableResolver | undefined, version: string | undefined, detectLocale: 'auto' | 'off' | 'on', baseEnv: IProcessEnvironment -): IProcessEnvironment { +): Promise<IProcessEnvironment> { // Create a terminal environment based on settings, launch config and permissions const env: IProcessEnvironment = {}; if (shellLaunchConfig.strictEnv) { @@ -379,10 +379,10 @@ export function createTerminalEnvironment( // Resolve env vars from config and shell if (variableResolver) { if (allowedEnvFromConfig) { - resolveConfigurationVariables(variableResolver, allowedEnvFromConfig); + await resolveConfigurationVariables(variableResolver, allowedEnvFromConfig); } if (shellLaunchConfig.env) { - resolveConfigurationVariables(variableResolver, shellLaunchConfig.env); + await resolveConfigurationVariables(variableResolver, shellLaunchConfig.env); } } diff --git a/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts b/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts index c38b20757e8..b2f01c3e853 100644 --- a/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts +++ b/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts @@ -250,9 +250,9 @@ class LocalTerminalBackend extends BaseTerminalBackend implements ITerminalBacke const platformKey = isWindows ? 'windows' : (isMacintosh ? 'osx' : 'linux'); const envFromConfigValue = this._configurationService.getValue<ITerminalEnvironment | undefined>(`terminal.integrated.env.${platformKey}`); const baseEnv = await (shellLaunchConfig.useShellEnvironment ? this.getShellEnvironment() : this.getEnvironment()); - const env = terminalEnvironment.createTerminalEnvironment(shellLaunchConfig, envFromConfigValue, variableResolver, this._productService.version, this._configurationService.getValue(TerminalSettingId.DetectLocale), baseEnv); + const env = await terminalEnvironment.createTerminalEnvironment(shellLaunchConfig, envFromConfigValue, variableResolver, this._productService.version, this._configurationService.getValue(TerminalSettingId.DetectLocale), baseEnv); if (!shellLaunchConfig.strictEnv && !shellLaunchConfig.hideFromUser) { - this._environmentVariableService.mergedCollection.applyToProcessEnvironment(env, variableResolver); + await this._environmentVariableService.mergedCollection.applyToProcessEnvironment(env, variableResolver); } return env; } diff --git a/src/vs/workbench/contrib/terminal/test/browser/terminalService.test.ts b/src/vs/workbench/contrib/terminal/test/browser/terminalService.test.ts index 9ab2dc7f95f..10ffee5b9d0 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/terminalService.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/terminalService.test.ts @@ -12,7 +12,7 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ContextKeyService } from 'vs/platform/contextkey/browser/contextKeyService'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { TestEditorService, TestLifecycleService, TestTerminalEditorService, TestTerminalGroupService, TestTerminalInstanceService, TestTerminalProfileService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestEditorService, TestLifecycleService, TestRemoteAgentService, TestTerminalEditorService, TestTerminalGroupService, TestTerminalInstanceService, TestTerminalProfileService } from 'vs/workbench/test/browser/workbenchTestServices'; import { ITerminalEditorService, ITerminalGroupService, ITerminalInstance, ITerminalInstanceService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -22,7 +22,6 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; -import { TestRemoteAgentService } from 'vs/workbench/services/remote/test/common/testServices'; suite('Workbench - TerminalService', () => { let instantiationService: TestInstantiationService; diff --git a/src/vs/workbench/contrib/terminal/test/common/environmentVariableCollection.test.ts b/src/vs/workbench/contrib/terminal/test/common/environmentVariableCollection.test.ts index 7b0b6594d4a..40c086a7dcc 100644 --- a/src/vs/workbench/contrib/terminal/test/common/environmentVariableCollection.test.ts +++ b/src/vs/workbench/contrib/terminal/test/common/environmentVariableCollection.test.ts @@ -78,7 +78,7 @@ suite('EnvironmentVariable - MergedEnvironmentVariableCollection', () => { }); suite('applyToProcessEnvironment', () => { - test('should apply the collection to an environment', () => { + test('should apply the collection to an environment', async () => { const merged = new MergedEnvironmentVariableCollection(new Map([ ['ext', { map: deserializeEnvironmentVariableCollection([ @@ -93,7 +93,7 @@ suite('EnvironmentVariable - MergedEnvironmentVariableCollection', () => { B: 'bar', C: 'baz' }; - merged.applyToProcessEnvironment(env); + await merged.applyToProcessEnvironment(env); deepStrictEqual(env, { A: 'a', B: 'barb', @@ -101,7 +101,7 @@ suite('EnvironmentVariable - MergedEnvironmentVariableCollection', () => { }); }); - test('should apply the collection to environment entries with no values', () => { + test('should apply the collection to environment entries with no values', async () => { const merged = new MergedEnvironmentVariableCollection(new Map([ ['ext', { map: deserializeEnvironmentVariableCollection([ @@ -112,7 +112,7 @@ suite('EnvironmentVariable - MergedEnvironmentVariableCollection', () => { }] ])); const env: IProcessEnvironment = {}; - merged.applyToProcessEnvironment(env); + await merged.applyToProcessEnvironment(env); deepStrictEqual(env, { A: 'a', B: 'b', @@ -120,7 +120,7 @@ suite('EnvironmentVariable - MergedEnvironmentVariableCollection', () => { }); }); - test('should apply to variable case insensitively on Windows only', () => { + test('should apply to variable case insensitively on Windows only', async () => { const merged = new MergedEnvironmentVariableCollection(new Map([ ['ext', { map: deserializeEnvironmentVariableCollection([ @@ -135,7 +135,7 @@ suite('EnvironmentVariable - MergedEnvironmentVariableCollection', () => { B: 'B', C: 'C' }; - merged.applyToProcessEnvironment(env); + await merged.applyToProcessEnvironment(env); if (isWindows) { deepStrictEqual(env, { A: 'a', diff --git a/src/vs/workbench/contrib/terminal/test/common/environmentVariableService.test.ts b/src/vs/workbench/contrib/terminal/test/common/environmentVariableService.test.ts index ed3269a6b83..dd391ea487d 100644 --- a/src/vs/workbench/contrib/terminal/test/common/environmentVariableService.test.ts +++ b/src/vs/workbench/contrib/terminal/test/common/environmentVariableService.test.ts @@ -87,7 +87,7 @@ suite('EnvironmentVariable - EnvironmentVariableService', () => { ]); }); - test('should correctly apply the environment values from multiple extension contributions in the correct order', () => { + test('should correctly apply the environment values from multiple extension contributions in the correct order', async () => { const collection1 = new Map<string, IEnvironmentVariableMutator>(); const collection2 = new Map<string, IEnvironmentVariableMutator>(); const collection3 = new Map<string, IEnvironmentVariableMutator>(); @@ -109,7 +109,7 @@ suite('EnvironmentVariable - EnvironmentVariableService', () => { // Verify the entries get applied to the environment as expected const env: IProcessEnvironment = { A: 'foo' }; - environmentVariableService.mergedCollection.applyToProcessEnvironment(env); + await environmentVariableService.mergedCollection.applyToProcessEnvironment(env); deepStrictEqual(env, { A: 'a2:a3:a1' }); }); }); diff --git a/src/vs/workbench/contrib/terminal/test/common/terminalEnvironment.test.ts b/src/vs/workbench/contrib/terminal/test/common/terminalEnvironment.test.ts index ca148531c12..b3918029cd0 100644 --- a/src/vs/workbench/contrib/terminal/test/common/terminalEnvironment.test.ts +++ b/src/vs/workbench/contrib/terminal/test/common/terminalEnvironment.test.ts @@ -180,66 +180,66 @@ suite('Workbench - TerminalEnvironment', () => { strictEqual(Uri.file(a).fsPath, Uri.file(b).fsPath); } - test('should default to userHome for an empty workspace', () => { - assertPathsMatch(getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, undefined, undefined), '/userHome/'); + test('should default to userHome for an empty workspace', async () => { + assertPathsMatch(await getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, undefined, undefined), '/userHome/'); }); - test('should use to the workspace if it exists', () => { - assertPathsMatch(getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, Uri.file('/foo'), undefined), '/foo'); + test('should use to the workspace if it exists', async () => { + assertPathsMatch(await getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, Uri.file('/foo'), undefined), '/foo'); }); - test('should use an absolute custom cwd as is', () => { - assertPathsMatch(getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, undefined, '/foo'), '/foo'); + test('should use an absolute custom cwd as is', async () => { + assertPathsMatch(await getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, undefined, '/foo'), '/foo'); }); - test('should normalize a relative custom cwd against the workspace path', () => { - assertPathsMatch(getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, Uri.file('/bar'), 'foo'), '/bar/foo'); - assertPathsMatch(getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, Uri.file('/bar'), './foo'), '/bar/foo'); - assertPathsMatch(getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, Uri.file('/bar'), '../foo'), '/foo'); + test('should normalize a relative custom cwd against the workspace path', async () => { + assertPathsMatch(await getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, Uri.file('/bar'), 'foo'), '/bar/foo'); + assertPathsMatch(await getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, Uri.file('/bar'), './foo'), '/bar/foo'); + assertPathsMatch(await getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, Uri.file('/bar'), '../foo'), '/foo'); }); - test('should fall back for relative a custom cwd that doesn\'t have a workspace', () => { - assertPathsMatch(getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, undefined, 'foo'), '/userHome/'); - assertPathsMatch(getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, undefined, './foo'), '/userHome/'); - assertPathsMatch(getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, undefined, '../foo'), '/userHome/'); + test('should fall back for relative a custom cwd that doesn\'t have a workspace', async () => { + assertPathsMatch(await getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, undefined, 'foo'), '/userHome/'); + assertPathsMatch(await getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, undefined, './foo'), '/userHome/'); + assertPathsMatch(await getCwd({ executable: undefined, args: [] }, '/userHome/', undefined, undefined, '../foo'), '/userHome/'); }); - test('should ignore custom cwd when told to ignore', () => { - assertPathsMatch(getCwd({ executable: undefined, args: [], ignoreConfigurationCwd: true }, '/userHome/', undefined, Uri.file('/bar'), '/foo'), '/bar'); + test('should ignore custom cwd when told to ignore', async () => { + assertPathsMatch(await getCwd({ executable: undefined, args: [], ignoreConfigurationCwd: true }, '/userHome/', undefined, Uri.file('/bar'), '/foo'), '/bar'); }); }); suite('getDefaultShell', () => { - test('should change Sysnative to System32 in non-WoW64 systems', () => { - const shell = getDefaultShell(key => { + test('should change Sysnative to System32 in non-WoW64 systems', async () => { + const shell = await getDefaultShell(key => { return ({ 'terminal.integrated.shell.windows': 'C:\\Windows\\Sysnative\\cmd.exe' } as any)[key]; }, 'DEFAULT', false, 'C:\\Windows', undefined, {} as any, false, Platform.Windows); strictEqual(shell, 'C:\\Windows\\System32\\cmd.exe'); }); - test('should not change Sysnative to System32 in WoW64 systems', () => { - const shell = getDefaultShell(key => { + test('should not change Sysnative to System32 in WoW64 systems', async () => { + const shell = await getDefaultShell(key => { return ({ 'terminal.integrated.shell.windows': 'C:\\Windows\\Sysnative\\cmd.exe' } as any)[key]; }, 'DEFAULT', true, 'C:\\Windows', undefined, {} as any, false, Platform.Windows); strictEqual(shell, 'C:\\Windows\\Sysnative\\cmd.exe'); }); - test('should use automationShell when specified', () => { - const shell1 = getDefaultShell(key => { + test('should use automationShell when specified', async () => { + const shell1 = await getDefaultShell(key => { return ({ 'terminal.integrated.shell.windows': 'shell', 'terminal.integrated.automationShell.windows': undefined } as any)[key]; }, 'DEFAULT', false, 'C:\\Windows', undefined, {} as any, false, Platform.Windows); strictEqual(shell1, 'shell', 'automationShell was false'); - const shell2 = getDefaultShell(key => { + const shell2 = await getDefaultShell(key => { return ({ 'terminal.integrated.shell.windows': 'shell', 'terminal.integrated.automationShell.windows': undefined } as any)[key]; }, 'DEFAULT', false, 'C:\\Windows', undefined, {} as any, true, Platform.Windows); strictEqual(shell2, 'shell', 'automationShell was true'); - const shell3 = getDefaultShell(key => { + const shell3 = await getDefaultShell(key => { return ({ 'terminal.integrated.shell.windows': 'shell', 'terminal.integrated.automationShell.windows': 'automationShell' diff --git a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts index 9382f47bc67..db687ec08b6 100644 --- a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts +++ b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts @@ -111,6 +111,21 @@ export class UnhideTestAction extends Action2 { } } +export class UnhideAllTestsAction extends Action2 { + constructor() { + super({ + id: TestCommandId.UnhideAllTestsAction, + title: localize('unhideAllTests', 'Unhide All Tests'), + }); + } + + public override run(accessor: ServicesAccessor) { + const service = accessor.get(ITestService); + service.excluded.clear(); + return Promise.resolve(); + } +} + const testItemInlineAndInContext = (order: ActionOrder, when?: ContextKeyExpression) => [ { id: MenuId.TestItem, @@ -1190,4 +1205,5 @@ export const allTestActions = [ TestingViewAsTreeAction, ToggleInlineTestOutput, UnhideTestAction, + UnhideAllTestsAction, ]; diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts index c32661787e3..4d1af363aa7 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts @@ -65,6 +65,11 @@ export class TestingExplorerFilter extends BaseActionViewItem { const wrapper = this.wrapper = dom.$('.testing-filter-wrapper'); container.appendChild(wrapper); + const history = this.history.get([]); + if (history.length) { + this.state.setText(history[history.length - 1]); + } + const input = this.input = this._register(this.instantiationService.createInstance(ContextScopedSuggestEnabledInputWithHistory, { id: 'testing.explorer.filter', ariaLabel: localize('testExplorerFilterLabel', "Filter text for tests in the explorer"), @@ -89,7 +94,7 @@ export class TestingExplorerFilter extends BaseActionViewItem { value: this.state.text.value, placeholderText: localize('testExplorerFilter', "Filter (e.g. text, !exclude, @tag)"), }, - history: this.history.get([]) + history })); this._register(attachSuggestEnabledInputBoxStyler(input, this.themeService)); diff --git a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts index 71b7930768c..979dc41d729 100644 --- a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts +++ b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts @@ -25,7 +25,7 @@ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Lazy } from 'vs/base/common/lazy'; import { Disposable, DisposableStore, IDisposable, IReference, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { clamp } from 'vs/base/common/numbers'; -import { count } from 'vs/base/common/strings'; +import { count, removeAnsiEscapeCodes } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { ICodeEditor, IDiffEditorConstructionOptions, isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction2 } from 'vs/editor/browser/editorExtensions'; @@ -1162,7 +1162,7 @@ class TestMessageElement implements ITreeElement { public readonly taskIndex: number, public readonly messageIndex: number, ) { - const { message, location } = test.tasks[taskIndex].messages[messageIndex]; + const { type, message, location } = test.tasks[taskIndex].messages[messageIndex]; this.location = location; this.uri = this.context = buildTestUri({ @@ -1175,7 +1175,9 @@ class TestMessageElement implements ITreeElement { this.id = this.uri.toString(); - const asPlaintext = renderStringAsPlaintext(message); + const asPlaintext = type === TestMessageType.Output + ? removeAnsiEscapeCodes(message) + : renderStringAsPlaintext(message); const lines = count(asPlaintext.trimRight(), '\n'); this.label = firstLine(asPlaintext); if (lines > 0) { diff --git a/src/vs/workbench/contrib/testing/browser/theme.ts b/src/vs/workbench/contrib/testing/browser/theme.ts index dcd6e2bb567..2a8d95b8665 100644 --- a/src/vs/workbench/contrib/testing/browser/theme.ts +++ b/src/vs/workbench/contrib/testing/browser/theme.ts @@ -91,7 +91,7 @@ export const testMessageSeverityColors: { localize('testing.message.error.marginBackground', 'Margin color beside error messages shown inline in the editor.') ), }, - [TestMessageType.Info]: { + [TestMessageType.Output]: { decorationForeground: registerColor( 'testing.message.info.decorationForeground', { dark: transparent(editorForeground, 0.5), light: transparent(editorForeground, 0.5), hcDark: transparent(editorForeground, 0.5), hcLight: transparent(editorForeground, 0.5) }, diff --git a/src/vs/workbench/contrib/testing/common/constants.ts b/src/vs/workbench/contrib/testing/common/constants.ts index c8fc457be2d..224cbd48e90 100644 --- a/src/vs/workbench/contrib/testing/common/constants.ts +++ b/src/vs/workbench/contrib/testing/common/constants.ts @@ -89,4 +89,5 @@ export const enum TestCommandId { ToggleAutoRun = 'testing.toggleautoRun', ToggleInlineTestOutput = 'testing.toggleInlineTestOutput', UnhideTestAction = 'testing.unhideTest', + UnhideAllTestsAction = 'testing.unhideAllTests', } diff --git a/src/vs/workbench/contrib/testing/common/testResult.ts b/src/vs/workbench/contrib/testing/common/testResult.ts index dce023d7d60..368d43988d7 100644 --- a/src/vs/workbench/contrib/testing/common/testResult.ts +++ b/src/vs/workbench/contrib/testing/common/testResult.ts @@ -321,7 +321,7 @@ export class LiveTestResult implements ITestResult { location, message: output.toString(), offset: this.output.offset, - type: TestMessageType.Info, + type: TestMessageType.Output, }; const index = this.mustGetTaskIndex(taskId); diff --git a/src/vs/workbench/contrib/testing/common/testTypes.ts b/src/vs/workbench/contrib/testing/common/testTypes.ts index 6f3e639ef90..29ae1628071 100644 --- a/src/vs/workbench/contrib/testing/common/testTypes.ts +++ b/src/vs/workbench/contrib/testing/common/testTypes.ts @@ -117,7 +117,7 @@ export namespace IRichLocation { export const enum TestMessageType { Error, - Info + Output } export interface ITestErrorMessage { @@ -156,7 +156,7 @@ export namespace ITestErrorMessage { export interface ITestOutputMessage { message: string; - type: TestMessageType.Info; + type: TestMessageType.Output; offset: number; location: IRichLocation | undefined; } @@ -165,20 +165,20 @@ export namespace ITestOutputMessage { export interface Serialized { message: string; offset: number; - type: TestMessageType.Info; + type: TestMessageType.Output; location: IRichLocation.Serialize | undefined; } export const serialize = (message: ITestOutputMessage): Serialized => ({ message: message.message, - type: TestMessageType.Info, + type: TestMessageType.Output, offset: message.offset, location: message.location && IRichLocation.serialize(message.location), }); export const deserialize = (message: Serialized): ITestOutputMessage => ({ message: message.message, - type: TestMessageType.Info, + type: TestMessageType.Output, offset: message.offset, location: message.location && IRichLocation.deserialize(message.location), }); diff --git a/src/vs/workbench/contrib/testing/common/testingContentProvider.ts b/src/vs/workbench/contrib/testing/common/testingContentProvider.ts index c93c0968a07..4ce00913d35 100644 --- a/src/vs/workbench/contrib/testing/common/testingContentProvider.ts +++ b/src/vs/workbench/contrib/testing/common/testingContentProvider.ts @@ -12,6 +12,7 @@ import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { TestMessageType } from 'vs/workbench/contrib/testing/common/testTypes'; import { parseTestUri, TestUriType, TEST_DATA_SCHEME } from 'vs/workbench/contrib/testing/common/testingUri'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; +import { removeAnsiEscapeCodes } from 'vs/base/common/strings'; /** * A content provider that returns various outputs for tests. This is used @@ -61,12 +62,14 @@ export class TestingContentProvider implements IWorkbenchContribution, ITextMode break; } case TestUriType.ResultMessage: { - const message = test.tasks[parsed.taskIndex].messages[parsed.messageIndex]?.message; - if (typeof message === 'string') { - text = message; - } else if (message) { - text = message.value; - language = this.languageService.createById('markdown'); + const message = test.tasks[parsed.taskIndex].messages[parsed.messageIndex]; + if (message) { + if (typeof message.message === 'string') { + text = message.type === TestMessageType.Output ? removeAnsiEscapeCodes(message.message) : message.message; + } else { + text = message.message.value; + language = this.languageService.createById('markdown'); + } } break; } diff --git a/src/vs/workbench/contrib/themes/test/electron-browser/colorRegistry.releaseTest.ts b/src/vs/workbench/contrib/themes/test/electron-browser/colorRegistry.releaseTest.ts index 397ecffdb59..76104ae81f2 100644 --- a/src/vs/workbench/contrib/themes/test/electron-browser/colorRegistry.releaseTest.ts +++ b/src/vs/workbench/contrib/themes/test/electron-browser/colorRegistry.releaseTest.ts @@ -5,7 +5,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IColorRegistry, Extensions, ColorContribution } from 'vs/platform/theme/common/colorRegistry'; -import { asText } from 'vs/platform/request/common/request'; +import { asTextOrError } from 'vs/platform/request/common/request'; import * as pfs from 'vs/base/node/pfs'; import * as path from 'vs/base/common/path'; import * as assert from 'assert'; @@ -39,7 +39,7 @@ suite('Color Registry', function () { const environmentService = new class extends mock<INativeEnvironmentService>() { override args = { _: [] }; }; const reqContext = await new RequestService(new TestConfigurationService(), environmentService, new NullLogService()).request({ url: 'https://raw.githubusercontent.com/microsoft/vscode-docs/vnext/api/references/theme-color.md' }, CancellationToken.None); - const content = (await asText(reqContext))!; + const content = (await asTextOrError(reqContext))!; const expression = /-\s*\`([\w\.]+)\`: (.*)/g; diff --git a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts index 98ed6664ed8..65b9d91beca 100644 --- a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts +++ b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts @@ -66,6 +66,7 @@ function isTimelineItem(item: TreeElement | undefined): item is TimelineItem { function updateRelativeTime(item: TimelineItem, lastRelativeTime: string | undefined): string | undefined { item.relativeTime = isTimelineItem(item) ? fromNow(item.timestamp) : undefined; + item.relativeTimeFullWord = isTimelineItem(item) ? fromNow(item.timestamp, false, true) : undefined; if (lastRelativeTime === undefined || item.relativeTime !== lastRelativeTime) { lastRelativeTime = item.relativeTime; item.hideRelativeTime = false; @@ -196,6 +197,7 @@ class LoadMoreCommand { readonly iconDark = undefined; readonly source = undefined; readonly relativeTime = undefined; + readonly relativeTimeFullWord = undefined; readonly hideRelativeTime = undefined; constructor(loading: boolean) { @@ -350,7 +352,7 @@ export class TimelinePane extends ViewPane { } private onActiveEditorChanged() { - if (!this.followActiveEditor) { + if (!this.followActiveEditor || !this.isExpanded()) { return; } @@ -569,9 +571,10 @@ export class TimelinePane extends ViewPane { } } request?.tokenSource.dispose(true); - + options.cacheResults = true; + options.resetCache = reset; request = this.timelineService.getTimeline( - source, uri, options, new CancellationTokenSource(), { cacheResults: true, resetCache: reset } + source, uri, options, new CancellationTokenSource() ); if (request === undefined) { @@ -885,7 +888,7 @@ export class TimelinePane extends ViewPane { if (isLoadMoreCommand(element)) { return element.ariaLabel; } - return element.accessibilityInformation ? element.accessibilityInformation.label : localize('timeline.aria.item', "{0}: {1}", element.relativeTime ?? '', element.label); + return element.accessibilityInformation ? element.accessibilityInformation.label : localize('timeline.aria.item', "{0}: {1}", element.relativeTimeFullWord ?? '', element.label); }, getRole(element: TreeElement): string { if (isLoadMoreCommand(element)) { @@ -1172,6 +1175,7 @@ class TimelineTreeRenderer implements ITreeRenderer<TreeElement, FuzzyScore, Tim }); template.timestamp.textContent = item.relativeTime ?? ''; + template.timestamp.ariaLabel = item.relativeTimeFullWord ?? ''; template.timestamp.parentElement!.classList.toggle('timeline-timestamp--duplicate', isTimelineItem(item) && item.hideRelativeTime); template.actionBar.context = { uri: this.uri, item: item } as TimelineActionContext; diff --git a/src/vs/workbench/contrib/timeline/common/timeline.ts b/src/vs/workbench/contrib/timeline/common/timeline.ts index edf2a850a02..5185f65703e 100644 --- a/src/vs/workbench/contrib/timeline/common/timeline.ts +++ b/src/vs/workbench/contrib/timeline/common/timeline.ts @@ -51,6 +51,7 @@ export interface TimelineItem { contextValue?: string; relativeTime?: string; + relativeTimeFullWord?: string; hideRelativeTime?: boolean; } @@ -76,11 +77,8 @@ export interface TimelineChangeEvent { export interface TimelineOptions { cursor?: string; limit?: number | { timestamp: number; id?: string }; -} - -export interface InternalTimelineOptions { - cacheResults: boolean; - resetCache: boolean; + resetCache?: boolean; + cacheResults?: boolean; } export interface Timeline { @@ -100,7 +98,7 @@ export interface Timeline { export interface TimelineProvider extends TimelineProviderDescriptor, IDisposable { onDidChange?: Event<TimelineChangeEvent>; - provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken, internalOptions?: InternalTimelineOptions): Promise<Timeline | undefined>; + provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken): Promise<Timeline | undefined>; } export interface TimelineSource { @@ -151,7 +149,7 @@ export interface ITimelineService { getSources(): TimelineSource[]; - getTimeline(id: string, uri: URI, options: TimelineOptions, tokenSource: CancellationTokenSource, internalOptions?: InternalTimelineOptions): TimelineRequest | undefined; + getTimeline(id: string, uri: URI, options: TimelineOptions, tokenSource: CancellationTokenSource): TimelineRequest | undefined; setUri(uri: URI): void; } diff --git a/src/vs/workbench/contrib/timeline/common/timelineService.ts b/src/vs/workbench/contrib/timeline/common/timelineService.ts index 52bc586f90d..190dfa2501c 100644 --- a/src/vs/workbench/contrib/timeline/common/timelineService.ts +++ b/src/vs/workbench/contrib/timeline/common/timelineService.ts @@ -8,7 +8,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { ILogService } from 'vs/platform/log/common/log'; -import { ITimelineService, TimelineChangeEvent, TimelineOptions, TimelineProvidersChangeEvent, TimelineProvider, InternalTimelineOptions, TimelinePaneId } from './timeline'; +import { ITimelineService, TimelineChangeEvent, TimelineOptions, TimelineProvidersChangeEvent, TimelineProvider, TimelinePaneId } from './timeline'; import { IViewsService } from 'vs/workbench/common/views'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; @@ -44,7 +44,7 @@ export class TimelineService implements ITimelineService { return [...this.providers.values()].map(p => ({ id: p.id, label: p.label })); } - getTimeline(id: string, uri: URI, options: TimelineOptions, tokenSource: CancellationTokenSource, internalOptions?: InternalTimelineOptions) { + getTimeline(id: string, uri: URI, options: TimelineOptions, tokenSource: CancellationTokenSource) { this.logService.trace(`TimelineService#getTimeline(${id}): uri=${uri.toString()}`); const provider = this.providers.get(id); @@ -61,7 +61,7 @@ export class TimelineService implements ITimelineService { } return { - result: provider.provideTimeline(uri, options, tokenSource.token, internalOptions) + result: provider.provideTimeline(uri, options, tokenSource.token) .then(result => { if (result === undefined) { return undefined; diff --git a/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts b/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts index f7379a4d721..df20df258da 100644 --- a/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts +++ b/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts @@ -21,7 +21,7 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IProductService } from 'vs/platform/product/common/productService'; -import { asText, IRequestService } from 'vs/platform/request/common/request'; +import { asTextOrError, IRequestService } from 'vs/platform/request/common/request'; import { DEFAULT_MARKDOWN_STYLES, renderMarkdownDocument } from 'vs/workbench/contrib/markdown/browser/markdownDocumentRenderer'; import { WebviewInput } from 'vs/workbench/contrib/webviewPanel/browser/webviewEditorInput'; import { IWebviewWorkbenchService } from 'vs/workbench/contrib/webviewPanel/browser/webviewWorkbenchService'; @@ -163,7 +163,7 @@ export class ReleaseNotesManager { const fetchReleaseNotes = async () => { let text; try { - text = await asText(await this._requestService.request({ url }, CancellationToken.None)); + text = await asTextOrError(await this._requestService.request({ url }, CancellationToken.None)); } catch { throw new Error('Failed to fetch release notes'); } diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index 07d50613857..4a7fe8b6202 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -37,7 +37,7 @@ import { EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/ed import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import * as Constants from 'vs/workbench/contrib/logs/common/logConstants'; -import { IOutputService } from 'vs/workbench/contrib/output/common/output'; +import { IOutputService } from 'vs/workbench/services/output/common/output'; import { IActivityService, IBadge, NumberBadge, ProgressBadge } from 'vs/workbench/services/activity/common/activity'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; diff --git a/src/vs/workbench/contrib/webview/browser/overlayWebview.ts b/src/vs/workbench/contrib/webview/browser/overlayWebview.ts index bb25dfdc171..112efec0ac3 100644 --- a/src/vs/workbench/contrib/webview/browser/overlayWebview.ts +++ b/src/vs/workbench/contrib/webview/browser/overlayWebview.ts @@ -21,7 +21,8 @@ export class OverlayWebview extends Disposable implements IOverlayWebview { private readonly _onDidWheel = this._register(new Emitter<IMouseWheelEvent>()); public readonly onDidWheel = this._onDidWheel.event; - private readonly _pendingMessages = new Set<{ readonly message: any; readonly transfer?: readonly ArrayBuffer[] }>(); + private _isFirstLoad = true; + private readonly _firstLoadPendingMessages = new Set<{ readonly message: any; readonly transfer?: readonly ArrayBuffer[]; readonly resolve: (value: boolean) => void }>(); private readonly _webview = this._register(new MutableDisposable<IWebviewElement>()); private readonly _webviewEvents = this._register(new DisposableStore()); @@ -70,6 +71,11 @@ export class OverlayWebview extends Disposable implements IOverlayWebview { this._container?.remove(); this._container = undefined; + for (const msg of this._firstLoadPendingMessages) { + msg.resolve(false); + } + this._firstLoadPendingMessages.clear(); + this._onDidDispose.fire(); super.dispose(); @@ -200,8 +206,13 @@ export class OverlayWebview extends Disposable implements IOverlayWebview { this._onDidUpdateState.fire(state); })); - this._pendingMessages.forEach(msg => webview.postMessage(msg.message, msg.transfer)); - this._pendingMessages.clear(); + if (this._isFirstLoad) { + this._firstLoadPendingMessages.forEach(async msg => { + msg.resolve(await webview.postMessage(msg.message, msg.transfer)); + }); + } + this._isFirstLoad = false; + this._firstLoadPendingMessages.clear(); } this.container.style.visibility = 'visible'; @@ -268,12 +279,19 @@ export class OverlayWebview extends Disposable implements IOverlayWebview { private readonly _onMissingCsp = this._register(new Emitter<ExtensionIdentifier>()); public readonly onMissingCsp: Event<any> = this._onMissingCsp.event; - public postMessage(message: any, transfer?: readonly ArrayBuffer[]): void { + public async postMessage(message: any, transfer?: readonly ArrayBuffer[]): Promise<boolean> { if (this._webview.value) { - this._webview.value.postMessage(message, transfer); - } else { - this._pendingMessages.add({ message, transfer }); + return this._webview.value.postMessage(message, transfer); } + + if (this._isFirstLoad) { + let resolve: (x: boolean) => void; + const p = new Promise<boolean>(r => resolve = r); + this._firstLoadPendingMessages.add({ message, transfer, resolve: resolve! }); + return p; + } + + return false; } focus(): void { this._webview.value?.focus(); } diff --git a/src/vs/workbench/contrib/webview/browser/pre/main.js b/src/vs/workbench/contrib/webview/browser/pre/main.js index 48beccb020a..d2c57f4aa10 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/main.js +++ b/src/vs/workbench/contrib/webview/browser/pre/main.js @@ -368,16 +368,14 @@ const unloadMonitor = new class { } switch (this.confirmBeforeClose) { - case 'always': - { - event.preventDefault(); - event.returnValue = ''; - return ''; - } - case 'never': - { - break; - } + case 'always': { + event.preventDefault(); + event.returnValue = ''; + return ''; + } + case 'never': { + break; + } case 'keyboardOnly': default: { if (this.isModifierKeyDown) { @@ -680,6 +678,22 @@ const handleInnerScroll = (event) => { }); }; +function handleInnerDragStartEvent(/** @type {DragEvent} */ e) { + if (e.defaultPrevented) { + // Extension code has already handled this event + return; + } + + if (!e.dataTransfer || e.shiftKey) { + return; + } + + // Only handle drags from outside editor for now + if (e.dataTransfer.items.length && Array.prototype.every.call(e.dataTransfer.items, item => item.kind === 'file')) { + hostMessaging.postMessage('drag-start'); + } +} + /** * @param {() => void} callback */ @@ -1021,6 +1035,9 @@ onDomReady(() => { }); }); + contentWindow.addEventListener('dragenter', handleInnerDragStartEvent); + contentWindow.addEventListener('dragover', handleInnerDragStartEvent); + unloadMonitor.onIframeLoaded(newFrame); } }); @@ -1107,5 +1124,10 @@ onDomReady(() => { } }; + // Also forward events before the contents of the webview have loaded + window.addEventListener('keydown', handleInnerKeydown); + window.addEventListener('dragenter', handleInnerDragStartEvent); + window.addEventListener('dragover', handleInnerDragStartEvent); + hostMessaging.signalReady(); }); diff --git a/src/vs/workbench/contrib/webview/browser/pre/service-worker.js b/src/vs/workbench/contrib/webview/browser/pre/service-worker.js index 70534ee470d..81a45ab6ca7 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/service-worker.js +++ b/src/vs/workbench/contrib/webview/browser/pre/service-worker.js @@ -189,8 +189,10 @@ sw.addEventListener('fetch', (event) => { } // If we're making a request against the remote authority, we want to go - // back through VS Code itself so that we are authenticated properly - if (requestUrl.host === remoteAuthority) { + // through VS Code itself so that we are authenticated properly. If the + // service worker is hosted on the same origin we will have cookies and + // authentication will not be an issue. + if (requestUrl.origin !== sw.origin && requestUrl.host === remoteAuthority) { switch (event.request.method) { case 'GET': case 'HEAD': diff --git a/src/vs/workbench/contrib/webview/browser/webview.ts b/src/vs/workbench/contrib/webview/browser/webview.ts index 0db26467cba..284a3d7f897 100644 --- a/src/vs/workbench/contrib/webview/browser/webview.ts +++ b/src/vs/workbench/contrib/webview/browser/webview.ts @@ -183,7 +183,7 @@ export interface IWebview extends IDisposable { readonly onMessage: Event<WebviewMessageReceivedEvent>; readonly onMissingCsp: Event<ExtensionIdentifier>; - postMessage(message: any, transfer?: readonly ArrayBuffer[]): void; + postMessage(message: any, transfer?: readonly ArrayBuffer[]): Promise<boolean>; focus(): void; reload(): void; diff --git a/src/vs/workbench/contrib/webview/browser/webviewElement.ts b/src/vs/workbench/contrib/webview/browser/webviewElement.ts index 1c312d73bdd..6cd87f63f28 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewElement.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { isFirefox } from 'vs/base/browser/browser'; -import { addDisposableListener } from 'vs/base/browser/dom'; +import { addDisposableListener, EventType } from 'vs/base/browser/dom'; import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; import { IAction } from 'vs/base/common/actions'; import { ThrottledDelayer } from 'vs/base/common/async'; @@ -59,6 +59,7 @@ export const enum WebviewMessageChannels { didKeydown = 'did-keydown', didKeyup = 'did-keyup', didContextMenu = 'did-context-menu', + dragStart = 'drag-start', } interface IKeydownEvent { @@ -85,10 +86,11 @@ namespace WebviewState { readonly type = Type.Initializing; constructor( - public readonly pendingMessages: Array<{ + public pendingMessages: Array<{ readonly channel: string; readonly data?: any; readonly transferable: Transferable[]; + readonly resolve: (posted: boolean) => void; }> ) { } } @@ -349,6 +351,10 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD } })); + this._register(this.on(WebviewMessageChannels.dragStart, () => { + this.startBlockingIframeDragEvents(); + })); + if (options.enableFindWidget) { this._webviewFindWidget = this._register(instantiationService.createInstance(WebviewFindWidget, this)); this.styledFindWidget(); @@ -369,6 +375,13 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD this.messagePort = undefined; + if (this._state.type === WebviewState.Type.Initializing) { + for (const message of this._state.pendingMessages) { + message.resolve(false); + } + this._state.pendingMessages = []; + } + this._onDidDispose.fire(); this._resourceLoadingCts.dispose(true); @@ -410,15 +423,18 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD private readonly _onDidDispose = this._register(new Emitter<void>()); public readonly onDidDispose = this._onDidDispose.event; - public postMessage(message: any, transfer?: ArrayBuffer[]): void { - this._send('message', { message, transfer }); + public postMessage(message: any, transfer?: ArrayBuffer[]): Promise<boolean> { + return this._send('message', { message, transfer }); } - protected _send(channel: string, data?: any, transferable: Transferable[] = []): void { + protected async _send(channel: string, data?: any, transferable: Transferable[] = []): Promise<boolean> { if (this._state.type === WebviewState.Type.Initializing) { - this._state.pendingMessages.push({ channel, data, transferable }); + let resolve: (x: boolean) => void; + const promise = new Promise<boolean>(r => resolve = r); + this._state.pendingMessages.push({ channel, data, transferable, resolve: resolve! }); + return promise; } else { - this.doPostMessage(channel, data, transferable); + return this.doPostMessage(channel, data, transferable); } } @@ -478,9 +494,32 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD if (this._webviewFindWidget) { parent.appendChild(this._webviewFindWidget.getDomNode()); } + + [EventType.MOUSE_DOWN, EventType.MOUSE_MOVE, EventType.DROP].forEach(eventName => { + this._register(addDisposableListener(parent, eventName, () => { + this.stopBlockingIframeDragEvents(); + })); + }); + + [parent, window].forEach(node => this._register(addDisposableListener(node as HTMLElement, EventType.DRAG_END, () => { + this.stopBlockingIframeDragEvents(); + }))); + parent.appendChild(this.element); } + private startBlockingIframeDragEvents() { + if (this.element) { + this.element.style.pointerEvents = 'none'; + } + } + + private stopBlockingIframeDragEvents() { + if (this.element) { + this.element.style.pointerEvents = 'auto'; + } + } + protected webviewContentEndpoint(encodedWebviewOrigin: string): string { const endpoint = this._environmentService.webviewExternalEndpoint!.replace('{{uuid}}', encodedWebviewOrigin); if (endpoint[endpoint.length - 1] === '/') { @@ -494,10 +533,12 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD return uri.scheme + '://' + uri.authority.toLowerCase(); } - private doPostMessage(channel: string, data?: any, transferable: Transferable[] = []): void { + private doPostMessage(channel: string, data?: any, transferable: Transferable[] = []): boolean { if (this.element && this.messagePort) { this.messagePort.postMessage({ channel, args: data }, transferable); + return true; } + return false; } protected on<T = unknown>(channel: WebviewMessageChannels, handler: (data: T, e: MessageEvent) => void): IDisposable { @@ -530,7 +571,9 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD } as const; type Classification = { - extension: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'mjbvz'; comment: 'The id of the extension that created the webview.' }; + extension: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The id of the extension that created the webview.' }; + owner: 'mjbz'; + comment: 'Helps find which extensions are contributing webviews with invalid CSPs'; }; this._telemetryService.publicLog2<typeof payload, Classification>('webviewMissingCsp', payload); @@ -670,18 +713,14 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD } windowDidDragStart(): void { - // Webview break drag and droping around the main window (no events are generated when you are over them) + // Webview break drag and dropping around the main window (no events are generated when you are over them) // Work around this by disabling pointer events during the drag. // https://github.com/electron/electron/issues/18226 - if (this.element) { - this.element.style.pointerEvents = 'none'; - } + this.startBlockingIframeDragEvents(); } windowDidDragEnd(): void { - if (this.element) { - this.element.style.pointerEvents = ''; - } + this.stopBlockingIframeDragEvents(); } public selectAll() { diff --git a/src/vs/workbench/contrib/webview/browser/webviewFindWidget.ts b/src/vs/workbench/contrib/webview/browser/webviewFindWidget.ts index e7bad42e252..58841297621 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewFindWidget.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewFindWidget.ts @@ -6,6 +6,7 @@ import { Event } from 'vs/base/common/event'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { SimpleFindWidget } from 'vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget'; import { KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED } from 'vs/workbench/contrib/webview/browser/webview'; @@ -29,9 +30,10 @@ export class WebviewFindWidget extends SimpleFindWidget { constructor( private readonly _delegate: WebviewFindDelegate, @IContextViewService contextViewService: IContextViewService, - @IContextKeyService contextKeyService: IContextKeyService + @IContextKeyService contextKeyService: IContextKeyService, + @IKeybindingService keybindingService: IKeybindingService ) { - super(contextViewService, contextKeyService, undefined, { showOptionButtons: false, checkImeCompletionState: _delegate.checkImeCompletionState }); + super(undefined, { showOptionButtons: false, checkImeCompletionState: _delegate.checkImeCompletionState }, contextViewService, contextKeyService, keybindingService); this._findWidgetFocused = KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED.bindTo(contextKeyService); this._register(_delegate.hasFindResult(hasResult => { diff --git a/src/vs/workbench/contrib/webviewPanel/browser/webviewEditorInput.ts b/src/vs/workbench/contrib/webviewPanel/browser/webviewEditorInput.ts index 1caa0e68f25..63e3f1cc124 100644 --- a/src/vs/workbench/contrib/webviewPanel/browser/webviewEditorInput.ts +++ b/src/vs/workbench/contrib/webviewPanel/browser/webviewEditorInput.ts @@ -23,7 +23,7 @@ export class WebviewInput extends EditorInput { } public override get capabilities(): EditorInputCapabilities { - return EditorInputCapabilities.Readonly | EditorInputCapabilities.Singleton; + return EditorInputCapabilities.Readonly | EditorInputCapabilities.Singleton | EditorInputCapabilities.CanDropIntoEditor; } private _name: string; diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts index 9784e9f9fd2..7015fa5e7b4 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts @@ -99,12 +99,16 @@ const parsedStartEntries: IWelcomePageStartEntry[] = startEntries.map((e, i) => })); type GettingStartedActionClassification = { - command: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; owner: 'JacksonKearl'; comment: 'Help understand what actions are most commonly taken on the getting started page' }; - argument: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; owner: 'JacksonKearl'; comment: 'As above' }; + command: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'The command being executed on the getting started page.' }; + walkthroughId: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'The walkthrough which the command is in' }; + argument: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'The arguments being passed to the command' }; + owner: 'lramos15'; + comment: 'Help understand what actions are most commonly taken on the getting started page'; }; type GettingStartedActionEvent = { command: string; + walkthroughId: string | undefined; argument: string | undefined; }; @@ -343,7 +347,7 @@ export class GettingStartedPage extends EditorPane { private async runDispatchCommand(command: string, argument: string) { this.commandService.executeCommand('workbench.action.keepEditor'); - this.telemetryService.publicLog2<GettingStartedActionEvent, GettingStartedActionClassification>('gettingStarted.ActionExecuted', { command, argument }); + this.telemetryService.publicLog2<GettingStartedActionEvent, GettingStartedActionClassification>('gettingStarted.ActionExecuted', { command, argument, walkthroughId: this.currentWalkthrough?.id }); switch (command) { case 'scrollPrev': { this.scrollPrev(); @@ -511,7 +515,7 @@ export class GettingStartedPage extends EditorPane { if (hrefs.length === 1) { const href = hrefs[0]; if (href.startsWith('http')) { - this.telemetryService.publicLog2<GettingStartedActionEvent, GettingStartedActionClassification>('gettingStarted.ActionExecuted', { command: 'runStepAction', argument: href }); + this.telemetryService.publicLog2<GettingStartedActionEvent, GettingStartedActionClassification>('gettingStarted.ActionExecuted', { command: 'runStepAction', argument: href, walkthroughId: this.currentWalkthrough?.id }); this.openerService.open(href); } } @@ -546,7 +550,7 @@ export class GettingStartedPage extends EditorPane { if (hrefs.length === 1) { const href = hrefs[0]; if (href.startsWith('http')) { - this.telemetryService.publicLog2<GettingStartedActionEvent, GettingStartedActionClassification>('gettingStarted.ActionExecuted', { command: 'runStepAction', argument: href }); + this.telemetryService.publicLog2<GettingStartedActionEvent, GettingStartedActionClassification>('gettingStarted.ActionExecuted', { command: 'runStepAction', argument: href, walkthroughId: this.currentWalkthrough?.id }); this.openerService.open(href); } } @@ -723,10 +727,10 @@ export class GettingStartedPage extends EditorPane { const showOnStartupLabel = $('label.caption', { for: 'showOnStartup' }, localize('welcomePage.showOnStartup', "Show welcome page on startup")); const onShowOnStartupChanged = () => { if (showOnStartupCheckbox.checked) { - this.telemetryService.publicLog2<GettingStartedActionEvent, GettingStartedActionClassification>('gettingStarted.ActionExecuted', { command: 'showOnStartupChecked', argument: undefined }); + this.telemetryService.publicLog2<GettingStartedActionEvent, GettingStartedActionClassification>('gettingStarted.ActionExecuted', { command: 'showOnStartupChecked', argument: undefined, walkthroughId: this.currentWalkthrough?.id }); this.configurationService.updateValue(configurationKey, 'welcomePage'); } else { - this.telemetryService.publicLog2<GettingStartedActionEvent, GettingStartedActionClassification>('gettingStarted.ActionExecuted', { command: 'showOnStartupUnchecked', argument: undefined }); + this.telemetryService.publicLog2<GettingStartedActionEvent, GettingStartedActionClassification>('gettingStarted.ActionExecuted', { command: 'showOnStartupUnchecked', argument: undefined, walkthroughId: this.currentWalkthrough?.id }); this.configurationService.updateValue(configurationKey, 'none'); } }; @@ -852,7 +856,7 @@ export class GettingStartedPage extends EditorPane { link.title = fullPath; link.setAttribute('aria-label', localize('welcomePage.openFolderWithPath', "Open folder {0} with path {1}", name, parentPath)); link.addEventListener('click', e => { - this.telemetryService.publicLog2<GettingStartedActionEvent, GettingStartedActionClassification>('gettingStarted.ActionExecuted', { command: 'openRecent', argument: undefined }); + this.telemetryService.publicLog2<GettingStartedActionEvent, GettingStartedActionClassification>('gettingStarted.ActionExecuted', { command: 'openRecent', argument: undefined, walkthroughId: this.currentWalkthrough?.id }); this.hostService.openWindow([windowOpenable], { forceNewWindow: e.ctrlKey || e.metaKey, remoteAuthority: recent.remoteAuthority || null // local window if remoteAuthority is not set or can not be deducted from the openable @@ -1092,7 +1096,7 @@ export class GettingStartedPage extends EditorPane { const toSide = href.startsWith('command:toSide:'); const command = href.replace(/command:(toSide:)?/, 'command:'); - this.telemetryService.publicLog2<GettingStartedActionEvent, GettingStartedActionClassification>('gettingStarted.ActionExecuted', { command: 'runStepAction', argument: href }); + this.telemetryService.publicLog2<GettingStartedActionEvent, GettingStartedActionClassification>('gettingStarted.ActionExecuted', { command: 'runStepAction', argument: href, walkthroughId: this.currentWalkthrough?.id }); const fullSize = this.groupsService.contentDimension; diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts index 75b3891db8d..4be26d8aedc 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts @@ -340,7 +340,7 @@ export class WalkthroughsService extends Disposable implements IWalkthroughsServ } if (step.media.image) { - const altText = (step.media as any).altText; + const altText = step.media.altText; if (altText === undefined) { console.error('Walkthrough item:', fullyQualifiedID, 'is missing altText for its media element.'); } @@ -362,7 +362,7 @@ export class WalkthroughsService extends Disposable implements IWalkthroughsServ }; } - // Legacy media config + // Legacy media config (only in use by remote-wsl at the moment) else { const legacyMedia = step.media as unknown as { path: string; altText: string }; if (typeof legacyMedia.path === 'string' && legacyMedia.path.endsWith('.md')) { diff --git a/src/vs/workbench/contrib/welcomeViews/common/newFile.contribution.ts b/src/vs/workbench/contrib/welcomeViews/common/newFile.contribution.ts index b78599440db..8b87c52e229 100644 --- a/src/vs/workbench/contrib/welcomeViews/common/newFile.contribution.ts +++ b/src/vs/workbench/contrib/welcomeViews/common/newFile.contribution.ts @@ -9,7 +9,7 @@ import { assertIsDefined } from 'vs/base/common/types'; import { localize } from 'vs/nls'; import { Action2, IMenuService, MenuId, registerAction2, IMenu, MenuRegistry, MenuItemAction } from 'vs/platform/actions/common/actions'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; @@ -18,11 +18,9 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; - +const builtInSource = localize('Built-In', "Built-In"); const category = localize('Create', "Create"); -export const HasMultipleNewFileEntries = new RawContextKey<boolean>('hasMultipleNewFileEntries', false); - registerAction2(class extends Action2 { constructor() { super({ @@ -36,15 +34,14 @@ registerAction2(class extends Action2 { }, menu: { id: MenuId.MenubarFileMenu, - when: HasMultipleNewFileEntries, group: '1_new', order: 2 } }); } - run(accessor: ServicesAccessor) { - assertIsDefined(NewFileTemplatesManager.Instance).run(); + async run(accessor: ServicesAccessor): Promise<boolean> { + return assertIsDefined(NewFileTemplatesManager.Instance).run(); } }); @@ -68,8 +65,6 @@ class NewFileTemplatesManager extends Disposable { this._register({ dispose() { if (NewFileTemplatesManager.Instance === this) { NewFileTemplatesManager.Instance = undefined; } } }); this.menu = menuService.createMenu(MenuId.NewFile, contextKeyService); - this.updateContextKeys(); - this._register(this.menu.onDidChange(() => { this.updateContextKeys(); })); } private allEntries(): NewFileItem[] { @@ -77,43 +72,53 @@ class NewFileTemplatesManager extends Disposable { for (const [groupName, group] of this.menu.getActions({ renderShortTitle: true })) { for (const action of group) { if (action instanceof MenuItemAction) { - items.push({ commandID: action.item.id, from: action.item.source ?? localize('Built-In', "Built-In"), title: action.label, group: groupName }); + items.push({ commandID: action.item.id, from: action.item.source ?? builtInSource, title: action.label, group: groupName }); } } } return items; } - private updateContextKeys() { - HasMultipleNewFileEntries.bindTo(this.contextKeyService).set(this.allEntries().length > 1); - } - - run() { + async run(): Promise<boolean> { const entries = this.allEntries(); if (entries.length === 0) { throw Error('Unexpected empty new items list'); } else if (entries.length === 1) { this.commandService.executeCommand(entries[0].commandID); + return true; } else { - this.selectNewEntry(entries); + return this.selectNewEntry(entries); } } - private async selectNewEntry(entries: NewFileItem[]) { + private async selectNewEntry(entries: NewFileItem[]): Promise<boolean> { + let resolveResult: (res: boolean) => void; + const resultPromise = new Promise<boolean>(resolve => { + resolveResult = resolve; + }); + const disposables = new DisposableStore(); const qp = this.quickInputService.createQuickPick(); qp.title = localize('createNew', "Create New..."); qp.matchOnDetail = true; qp.matchOnDescription = true; - const sortCategories = (a: string, b: string): number => { + const sortCategories = (a: NewFileItem, b: NewFileItem): number => { const categoryPriority: Record<string, number> = { 'file': 1, 'notebook': 2 }; - if (categoryPriority[a] && categoryPriority[b]) { return categoryPriority[b] - categoryPriority[a]; } - if (categoryPriority[a]) { return 1; } - if (categoryPriority[b]) { return -1; } - return a.localeCompare(b); + if (categoryPriority[a.group] && categoryPriority[b.group]) { + if (categoryPriority[a.group] !== categoryPriority[b.group]) { + return categoryPriority[b.group] - categoryPriority[a.group]; + } + } + else if (categoryPriority[a.group]) { return 1; } + else if (categoryPriority[b.group]) { return -1; } + + if (a.from === builtInSource) { return 1; } + if (b.from === builtInSource) { return -1; } + + return a.from.localeCompare(b.from); }; const displayCategory: Record<string, string> = { @@ -125,7 +130,7 @@ class NewFileTemplatesManager extends Disposable { const items: (((IQuickPickItem & NewFileItem) | IQuickPickSeparator))[] = []; let lastSeparator: string | undefined; entries - .sort((a, b) => -sortCategories(a.group, b.group)) + .sort((a, b) => -sortCategories(a, b)) .forEach((entry) => { const command = entry.commandID; const keybinding = this.keybindingService.lookupKeybinding(command || '', this.contextKeyService); @@ -159,6 +164,8 @@ class NewFileTemplatesManager extends Disposable { disposables.add(qp.onDidAccept(async e => { const selected = qp.selectedItems[0] as (IQuickPickItem & NewFileItem); + resolveResult(!!selected); + qp.hide(); if (selected) { await this.commandService.executeCommand(selected.commandID); } })); @@ -166,23 +173,26 @@ class NewFileTemplatesManager extends Disposable { disposables.add(qp.onDidHide(() => { qp.dispose(); disposables.dispose(); + resolveResult(false); })); disposables.add(qp.onDidTriggerItemButton(e => { qp.hide(); this.commandService.executeCommand('workbench.action.openGlobalKeybindings', (e.item as (IQuickPickItem & NewFileItem)).commandID); + resolveResult(false); })); qp.show(); - } + return resultPromise; + } } Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench) .registerWorkbenchContribution(NewFileTemplatesManager, LifecyclePhase.Restored); MenuRegistry.appendMenuItem(MenuId.NewFile, { - group: 'File', + group: 'file', command: { id: 'workbench.action.files.newUntitledFile', title: localize('miNewFile2', "Text File") diff --git a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts index 714d83dc166..1575fbf2118 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts @@ -761,7 +761,9 @@ class WorkspaceTrustTelemetryContribution extends Disposable implements IWorkben const disabledByCliFlag = this.environmentService.disableWorkspaceTrust; type WorkspaceTrustDisabledEventClassification = { - reason: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; + owner: 'sbatten'; + comment: 'Logged when workspace trust is disabled'; + reason: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The reason workspace trust is disabled. e.g. cli or setting' }; }; type WorkspaceTrustDisabledEvent = { @@ -775,7 +777,9 @@ class WorkspaceTrustTelemetryContribution extends Disposable implements IWorkben } type WorkspaceTrustInfoEventClassification = { - trustedFoldersCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; + owner: 'sbatten'; + comment: 'Information about the workspaces trusted on the machine'; + trustedFoldersCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The number of trusted folders on the machine' }; }; type WorkspaceTrustInfoEvent = { @@ -798,8 +802,10 @@ class WorkspaceTrustTelemetryContribution extends Disposable implements IWorkben }; type WorkspaceTrustStateChangedEventClassification = { - workspaceId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; - isTrusted: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; + owner: 'sbatten'; + comment: 'Logged when the workspace transitions between trusted and restricted modes'; + workspaceId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'An id of the workspace' }; + isTrusted: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'true if the workspace is trusted' }; }; this.telemetryService.publicLog2<WorkspaceTrustStateChangedEvent, WorkspaceTrustStateChangedEventClassification>('workspaceTrustStateChanged', { @@ -809,9 +815,11 @@ class WorkspaceTrustTelemetryContribution extends Disposable implements IWorkben if (isTrusted) { type WorkspaceTrustFolderInfoEventClassification = { - trustedFolderDepth: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; - workspaceFolderDepth: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; - delta: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; + owner: 'sbatten'; + comment: 'Some metrics on the trusted workspaces folder structure'; + trustedFolderDepth: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The number of directories deep of the trusted path' }; + workspaceFolderDepth: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The number of directories deep of the workspace path' }; + delta: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The difference between the trusted path and the workspace path directories depth' }; }; type WorkspaceTrustFolderInfoEvent = { diff --git a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts index 5aae1567d83..679be9104d5 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts @@ -50,7 +50,8 @@ import { WorkspaceTrustEditorInput } from 'vs/workbench/services/workspaces/brow import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { getExtensionDependencies } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { EnablementState, IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -import { posix } from 'vs/base/common/path'; +import { posix, win32 } from 'vs/base/common/path'; +import { hasDriveLetter, toSlashes } from 'vs/base/common/extpath'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { IProductService } from 'vs/platform/product/common/productService'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; @@ -352,7 +353,6 @@ class WorkspaceTrustedUrisTable extends Disposable { canSelectMany: false, defaultUri: item.uri, openLabel: localize('trustUri', "Trust Folder"), - title: localize('selectTrustedUri', "Select Folder To Trust") }); @@ -530,8 +530,10 @@ class TrustedUriPathColumnRenderer implements ITableRenderer<ITrustedUriItem, IT const accept = () => { hideInputBox(); - const uri = item.uri.with({ path: templateData.pathInput.value }); - templateData.pathLabel.innerText = templateData.pathInput.value; + + const pathToUse = templateData.pathInput.value; + const uri = hasDriveLetter(pathToUse) ? item.uri.with({ path: posix.sep + toSlashes(pathToUse) }) : item.uri.with({ path: pathToUse }); + templateData.pathLabel.innerText = this.formatPath(uri); if (uri) { this.table.acceptEdit(item, uri); @@ -563,12 +565,10 @@ class TrustedUriPathColumnRenderer implements ITableRenderer<ITrustedUriItem, IT reject(); }))); - const stringValue = item.uri.scheme === Schemas.file ? URI.revive(item.uri).fsPath : item.uri.path; + const stringValue = this.formatPath(item.uri); templateData.pathInput.value = stringValue; templateData.pathLabel.innerText = stringValue; templateData.element.classList.toggle('current-workspace-parent', item.parentOfWorkspaceItem); - - // templateData.pathLabel.style.display = ''; } disposeTemplate(templateData: ITrustedUriPathColumnTemplateData): void { @@ -576,6 +576,24 @@ class TrustedUriPathColumnRenderer implements ITableRenderer<ITrustedUriItem, IT templateData.renderDisposables.dispose(); } + private formatPath(uri: URI): string { + if (uri.scheme === Schemas.file) { + return uri.fsPath; + } + + // If the path is not a file uri, but points to a windows remote, we should create windows fs path + // e.g. /c:/user/directory => C:\user\directory + if (uri.path.startsWith(posix.sep)) { + const pathWithoutLeadingSeparator = uri.path.substring(1); + const isWindowsPath = hasDriveLetter(pathWithoutLeadingSeparator, true); + if (isWindowsPath) { + return win32.normalize(pathWithoutLeadingSeparator); + } + } + + return uri.path; + } + } diff --git a/src/vs/workbench/electron-sandbox/actions/windowActions.ts b/src/vs/workbench/electron-sandbox/actions/windowActions.ts index 8ab7286bcfe..a89681fa292 100644 --- a/src/vs/workbench/electron-sandbox/actions/windowActions.ts +++ b/src/vs/workbench/electron-sandbox/actions/windowActions.ts @@ -227,6 +227,7 @@ abstract class BaseSwitchWindow extends Action2 { activeItem: picks[autoFocusIndex], placeHolder, quickNavigate: this.isQuickNavigate() ? { keybindings: keybindingService.lookupKeybindings(this.desc.id) } : undefined, + hideInput: this.isQuickNavigate(), onDidTriggerItemButton: async context => { await nativeHostService.closeWindowById(context.item.payload); context.removeItem(); diff --git a/src/vs/workbench/electron-sandbox/desktop.contribution.ts b/src/vs/workbench/electron-sandbox/desktop.contribution.ts index c939e4c6c80..8c88a219e24 100644 --- a/src/vs/workbench/electron-sandbox/desktop.contribution.ts +++ b/src/vs/workbench/electron-sandbox/desktop.contribution.ts @@ -22,6 +22,10 @@ import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { InstallShellScriptAction, UninstallShellScriptAction } from 'vs/workbench/electron-sandbox/actions/installActions'; import { EditorsVisibleContext, SingleEditorGroupsContext } from 'vs/workbench/common/contextkeys'; import { TELEMETRY_SETTING_ID } from 'vs/platform/telemetry/common/telemetry'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ShutdownReason } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { NativeWindow } from 'vs/workbench/electron-sandbox/window'; +import { ModifierKeyEmitter } from 'vs/base/browser/dom'; // Actions (function registerActions(): void { @@ -59,8 +63,18 @@ import { TELEMETRY_SETTING_ID } from 'vs/platform/telemetry/common/telemetry'; KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'workbench.action.quit', weight: KeybindingWeight.WorkbenchContrib, - handler(accessor: ServicesAccessor) { + async handler(accessor: ServicesAccessor) { const nativeHostService = accessor.get(INativeHostService); + const configurationService = accessor.get(IConfigurationService); + + const confirmBeforeClose = configurationService.getValue<'always' | 'never' | 'keyboardOnly'>('window.confirmBeforeClose'); + if (confirmBeforeClose === 'always' || (confirmBeforeClose === 'keyboardOnly' && ModifierKeyEmitter.getInstance().isModifierPressed)) { + const confirmed = await NativeWindow.confirmOnShutdown(accessor, ShutdownReason.QUIT); + if (!confirmed) { + return; // quit prevented by user + } + } + nativeHostService.quit(); }, when: undefined, @@ -70,21 +84,21 @@ import { TELEMETRY_SETTING_ID } from 'vs/platform/telemetry/common/telemetry'; // Actions: macOS Native Tabs if (isMacintosh) { - [ + for (const command of [ { handler: NewWindowTabHandler, id: 'workbench.action.newWindowTab', title: { value: localize('newTab', "New Window Tab"), original: 'New Window Tab' } }, { handler: ShowPreviousWindowTabHandler, id: 'workbench.action.showPreviousWindowTab', title: { value: localize('showPreviousTab', "Show Previous Window Tab"), original: 'Show Previous Window Tab' } }, { handler: ShowNextWindowTabHandler, id: 'workbench.action.showNextWindowTab', title: { value: localize('showNextWindowTab', "Show Next Window Tab"), original: 'Show Next Window Tab' } }, { handler: MoveWindowTabToNewWindowHandler, id: 'workbench.action.moveWindowTabToNewWindow', title: { value: localize('moveWindowTabToNewWindow', "Move Window Tab to New Window"), original: 'Move Window Tab to New Window' } }, { handler: MergeWindowTabsHandlerHandler, id: 'workbench.action.mergeAllWindowTabs', title: { value: localize('mergeAllWindowTabs', "Merge All Windows"), original: 'Merge All Windows' } }, { handler: ToggleWindowTabsBarHandler, id: 'workbench.action.toggleWindowTabsBar', title: { value: localize('toggleWindowTabsBar', "Toggle Window Tabs Bar"), original: 'Toggle Window Tabs Bar' } } - ].forEach(command => { + ]) { CommandsRegistry.registerCommand(command.id, command.handler); MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command, when: ContextKeyExpr.equals('config.window.nativeTabs', true) }); - }); + } } // Actions: Developer diff --git a/src/vs/workbench/electron-sandbox/desktop.main.ts b/src/vs/workbench/electron-sandbox/desktop.main.ts index 6a78e65d27c..7e658a10cdc 100644 --- a/src/vs/workbench/electron-sandbox/desktop.main.ts +++ b/src/vs/workbench/electron-sandbox/desktop.main.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from 'vs/nls'; import product from 'vs/platform/product/common/product'; import { INativeWindowConfiguration, zoomLevelToZoomFactor } from 'vs/platform/window/common/window'; import { Workbench } from 'vs/workbench/browser/workbench'; @@ -83,15 +84,15 @@ export class DesktopMain extends Disposable { // Files const filesToWait = this.configuration.filesToWait; const filesToWaitPaths = filesToWait?.paths; - [filesToWaitPaths, this.configuration.filesToOpenOrCreate, this.configuration.filesToDiff].forEach(paths => { + for (const paths of [filesToWaitPaths, this.configuration.filesToOpenOrCreate, this.configuration.filesToDiff]) { if (Array.isArray(paths)) { - paths.forEach(path => { + for (const path of paths) { if (path.fileUri) { path.fileUri = URI.revive(path.fileUri); } - }); + } } - }); + } if (filesToWait) { filesToWait.waitMarkerFileUri = URI.revive(filesToWait.waitMarkerFileUri); @@ -129,7 +130,7 @@ export class DesktopMain extends Disposable { private registerListeners(workbench: Workbench, storageService: NativeStorageService): void { // Workbench Lifecycle - this._register(workbench.onWillShutdown(event => event.join(storageService.close(), 'join.closeStorage'))); + this._register(workbench.onWillShutdown(event => event.join(storageService.close(), { id: 'join.closeStorage', label: localize('join.closeStorage', "Saving UI state") }))); this._register(workbench.onDidShutdown(() => this.dispose())); } @@ -278,7 +279,7 @@ export class DesktopMain extends Disposable { const workspaceTrustEnablementService = new WorkspaceTrustEnablementService(configurationService, environmentService); serviceCollection.set(IWorkspaceTrustEnablementService, workspaceTrustEnablementService); - const workspaceTrustManagementService = new WorkspaceTrustManagementService(configurationService, remoteAuthorityResolverService, storageService, uriIdentityService, environmentService, configurationService, workspaceTrustEnablementService, logService); + const workspaceTrustManagementService = new WorkspaceTrustManagementService(configurationService, remoteAuthorityResolverService, storageService, uriIdentityService, environmentService, configurationService, workspaceTrustEnablementService); serviceCollection.set(IWorkspaceTrustManagementService, workspaceTrustManagementService); // Update workspace trust so that configuration is updated accordingly diff --git a/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts b/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts index 0afcfd8ade7..7a7858fd9c4 100644 --- a/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { getZoomFactor } from 'vs/base/browser/browser'; -import { $, addDisposableListener, append, Dimension, EventType, hide, prepend, runAtThisOrScheduleAtNextAnimationFrame, show } from 'vs/base/browser/dom'; +import { $, addDisposableListener, append, EventType, hide, prepend, show } from 'vs/base/browser/dom'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { ILabelService } from 'vs/platform/label/common/label'; @@ -25,11 +25,13 @@ import { getTitleBarStyle } from 'vs/platform/window/common/window'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Codicon } from 'vs/base/common/codicons'; import { NativeMenubarControl } from 'vs/workbench/electron-sandbox/parts/titlebar/menubarControl'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; export class TitlebarPart extends BrowserTitleBarPart { private maxRestoreControl: HTMLElement | undefined; private dragRegion: HTMLElement | undefined; private resizer: HTMLElement | undefined; + private cachedWindowControlStyles: { bgColor: string; fgColor: string } | undefined; private getMacTitlebarSize() { const osVersion = this.environmentService.os.release; @@ -60,9 +62,10 @@ export class TitlebarPart extends BrowserTitleBarPart { @IContextKeyService contextKeyService: IContextKeyService, @IHostService hostService: IHostService, @IProductService productService: IProductService, - @INativeHostService private readonly nativeHostService: INativeHostService + @INativeHostService private readonly nativeHostService: INativeHostService, + @IKeybindingService keybindingService: IKeybindingService, ) { - super(contextMenuService, configurationService, editorService, environmentService, contextService, instantiationService, themeService, labelService, storageService, layoutService, menuService, contextKeyService, hostService, productService); + super(contextMenuService, configurationService, editorService, environmentService, contextService, instantiationService, themeService, labelService, storageService, layoutService, menuService, contextKeyService, hostService, productService, keybindingService); this.environmentService = environmentService; } @@ -131,28 +134,6 @@ export class TitlebarPart extends BrowserTitleBarPart { } } - protected override adjustTitleMarginToCenter(): void { - if (this.customMenubar && this.menubar) { - const leftMarker = (this.appIcon ? this.appIcon.clientWidth : 0) + this.menubar.clientWidth + 10; - const rightMarker = this.element.clientWidth - (this.windowControls ? this.windowControls.clientWidth : 0) - 10; - - // Not enough space to center the titlebar within window, - // Center between menu and window controls - if (leftMarker > (this.element.clientWidth - this.title.clientWidth) / 2 || - rightMarker < (this.element.clientWidth + this.title.clientWidth) / 2) { - this.title.style.position = ''; - this.title.style.left = ''; - this.title.style.transform = ''; - return; - } - } - - this.title.style.position = 'absolute'; - this.title.style.left = '50%'; - this.title.style.transform = 'translate(-50%, 0)'; - this.title.style.maxWidth = `calc(100vw - ${2 * ((this.windowControls?.clientWidth || 70) + 10)}px)`; - } - protected override installMenubar(): void { super.installMenubar(); @@ -186,7 +167,8 @@ export class TitlebarPart extends BrowserTitleBarPart { this.dragRegion = prepend(this.rootContainer, $('div.titlebar-drag-region')); // Window Controls (Native Windows/Linux) - if (!isMacintosh && this.windowControls) { + const hasWindowControlsOverlay = typeof (navigator as any).windowControlsOverlay !== 'undefined'; + if (!isMacintosh && getTitleBarStyle(this.configurationService) !== 'native' && !hasWindowControlsOverlay && this.windowControls) { // Minimize const minimizeIcon = append(this.windowControls, $('div.window-icon.window-minimize' + Codicon.chromeMinimize.cssSelector)); this._register(addDisposableListener(minimizeIcon, EventType.CLICK, e => { @@ -220,25 +202,15 @@ export class TitlebarPart extends BrowserTitleBarPart { return ret; } - override updateLayout(dimension: Dimension): void { - this.lastLayoutDimensions = dimension; - - if (getTitleBarStyle(this.configurationService) === 'custom') { - if (isMacintosh || this.currentMenubarVisibility === 'hidden') { - this.rootContainer.style.height = `${100.0 * getZoomFactor()}%`; - this.rootContainer.style.width = `${100.0 * getZoomFactor()}%`; - this.rootContainer.style.transform = `scale(${1 / getZoomFactor()})`; - } else { - this.rootContainer.style.height = `100%`; - this.rootContainer.style.width = `100%`; - this.rootContainer.style.transform = ''; - } - - runAtThisOrScheduleAtNextAnimationFrame(() => this.adjustTitleMarginToCenter()); + override updateStyles(): void { + super.updateStyles(); - if (this.customMenubar) { - const menubarDimension = new Dimension(0, dimension.height); - this.customMenubar.layout(menubarDimension); + // WCO styles only supported on Windows currently + if (isWindows) { + if (!this.cachedWindowControlStyles || + this.cachedWindowControlStyles.bgColor !== this.element.style.backgroundColor || + this.cachedWindowControlStyles.fgColor !== this.element.style.color) { + this.nativeHostService.updateTitleBarOverlay(this.element.style.backgroundColor, this.element.style.color); } } } diff --git a/src/vs/workbench/electron-sandbox/window.ts b/src/vs/workbench/electron-sandbox/window.ts index 389c1218e67..231433ee322 100644 --- a/src/vs/workbench/electron-sandbox/window.ts +++ b/src/vs/workbench/electron-sandbox/window.ts @@ -7,7 +7,7 @@ import { localize } from 'vs/nls'; import { URI } from 'vs/base/common/uri'; import { onUnexpectedError } from 'vs/base/common/errors'; import { equals } from 'vs/base/common/objects'; -import { EventType, EventHelper, addDisposableListener, scheduleAtNextAnimationFrame } from 'vs/base/browser/dom'; +import { EventType, EventHelper, addDisposableListener, scheduleAtNextAnimationFrame, ModifierKeyEmitter } from 'vs/base/browser/dom'; import { Separator, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions'; import { IFileService } from 'vs/platform/files/common/files'; import { EditorResourceAccessor, IUntitledTextResourceEditorInput, SideBySideEditor, pathsToEditors, IResourceDiffEditorInput, IUntypedEditorInput } from 'vs/workbench/common/editor'; @@ -44,8 +44,7 @@ import { assertIsDefined, isArray } from 'vs/base/common/types'; import { IOpenerService, OpenOptions } from 'vs/platform/opener/common/opener'; import { Schemas } from 'vs/base/common/network'; import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; -import { posix, dirname } from 'vs/base/common/path'; -import { getBaseLabel } from 'vs/base/common/labels'; +import { posix } from 'vs/base/common/path'; import { ITunnelService, extractLocalHostUriMetaDataForPortMapping } from 'vs/platform/tunnel/common/tunnel'; import { IWorkbenchLayoutService, Parts, positionFromString, Position } from 'vs/workbench/services/layout/browser/layoutService'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; @@ -58,12 +57,14 @@ import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editor import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { AuthInfo } from 'vs/base/parts/sandbox/electron-sandbox/electronTypes'; import { ILogService } from 'vs/platform/log/common/log'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { whenEditorClosed } from 'vs/workbench/browser/editor'; import { ISharedProcessService } from 'vs/platform/ipc/electron-sandbox/services'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; import { toErrorMessage } from 'vs/base/common/errorMessage'; -import { registerLegacyWindowDriver, registerWindowDriver } from 'vs/platform/driver/electron-sandbox/driver'; +import { registerWindowDriver } from 'vs/platform/driver/electron-sandbox/driver'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { dirname } from 'vs/base/common/resources'; export class NativeWindow extends Disposable { @@ -115,7 +116,8 @@ export class NativeWindow extends Disposable { @ILogService private readonly logService: ILogService, @IInstantiationService private readonly instantiationService: IInstantiationService, @ISharedProcessService private readonly sharedProcessService: ISharedProcessService, - @IProgressService private readonly progressService: IProgressService + @IProgressService private readonly progressService: IProgressService, + @ILabelService private readonly labelService: ILabelService ) { super(); @@ -132,11 +134,11 @@ export class NativeWindow extends Disposable { this._register(this.editorService.onDidActiveEditorChange(() => this.updateTouchbarMenu())); // prevent opening a real URL inside the window - [EventType.DRAG_OVER, EventType.DROP].forEach(event => { + for (const event of [EventType.DRAG_OVER, EventType.DROP]) { window.document.body.addEventListener(event, (e: DragEvent) => { EventHelper.stop(e); }); - }); + } // Support runAction event ipcRenderer.on('vscode:runAction', async (event: unknown, request: INativeRunActionInWindowRequest) => { @@ -332,7 +334,46 @@ export class NativeWindow extends Disposable { this._register(this.lifecycleService.onWillShutdown(e => this.onWillShutdown(e))); } - private onBeforeShutdown({ reason }: BeforeShutdownEvent): void { + private onBeforeShutdown({ veto, reason }: BeforeShutdownEvent): void { + if (reason === ShutdownReason.CLOSE) { + const confirmBeforeCloseSetting = this.configurationService.getValue<'always' | 'never' | 'keyboardOnly'>('window.confirmBeforeClose'); + + const confirmBeforeClose = confirmBeforeCloseSetting === 'always' || (confirmBeforeCloseSetting === 'keyboardOnly' && ModifierKeyEmitter.getInstance().isModifierPressed); + if (confirmBeforeClose) { + + // When we need to confirm on close or quit, veto the shutdown + // with a long running promise to figure out whether shutdown + // can proceed or not. + + return veto((async () => { + let actualReason: ShutdownReason = reason; + if (reason === ShutdownReason.CLOSE && !isMacintosh) { + const windowCount = await this.nativeHostService.getWindowCount(); + if (windowCount === 1) { + actualReason = ShutdownReason.QUIT; // Windows/Linux: closing last window means to QUIT + } + } + + let confirmed = true; + if (confirmBeforeClose) { + confirmed = await this.instantiationService.invokeFunction(accessor => NativeWindow.confirmOnShutdown(accessor, actualReason)); + } + + // Progress for long running shutdown + if (confirmed) { + this.progressOnBeforeShutdown(reason); + } + + return !confirmed; + })(), 'veto.confirmBeforeClose'); + } + } + + // Progress for long running shutdown + this.progressOnBeforeShutdown(reason); + } + + private progressOnBeforeShutdown(reason: ShutdownReason): void { this.progressService.withProgress({ location: ProgressLocation.Window, // use window progress to not be too annoying about this operation delay: 800, // delay so that it only appears when operation takes a long time @@ -346,25 +387,63 @@ export class NativeWindow extends Disposable { }); } + static async confirmOnShutdown(accessor: ServicesAccessor, reason: ShutdownReason): Promise<boolean> { + const dialogService = accessor.get(IDialogService); + const configurationService = accessor.get(IConfigurationService); + + const message = reason === ShutdownReason.QUIT ? + (isMacintosh ? localize('quitMessageMac', "Are you sure you want to quit?") : localize('quitMessage', "Are you sure you want to exit?")) : + localize('closeWindowMessage', "Are you sure you want to close the window?"); + const primaryButton = reason === ShutdownReason.QUIT ? + (isMacintosh ? localize({ key: 'quitButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Quit") : localize({ key: 'exitButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Exit")) : + localize({ key: 'closeWindowButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Close Window"); + + const res = await dialogService.confirm({ + type: 'question', + message, + primaryButton, + checkbox: { + label: localize('doNotAskAgain', "Do not ask me again") + } + }); + + // Update setting if checkbox checked + if (res.checkboxChecked) { + await configurationService.updateValue('window.confirmBeforeClose', 'never'); + } + + return res.confirmed; + } + private onBeforeShutdownError({ error, reason }: BeforeShutdownErrorEvent): void { this.dialogService.show(Severity.Error, this.toShutdownLabel(reason, true), undefined, { detail: localize('shutdownErrorDetail', "Error: {0}", toErrorMessage(error)) }); } - private onWillShutdown({ reason, force }: WillShutdownEvent): void { - this.progressService.withProgress({ - location: ProgressLocation.Dialog, // use a dialog to prevent the user from making any more interactions now - buttons: [this.toForceShutdownLabel(reason)], // allow to force shutdown anyway - delay: 800, // delay so that it only appears when operation takes a long time - cancellable: false, // do not allow to cancel - sticky: true, // do not allow to dismiss - title: this.toShutdownLabel(reason, false) - }, () => { - return Event.toPromise(this.lifecycleService.onDidShutdown); // dismiss this dialog when we actually shutdown - }, () => { - force(); - }); + private onWillShutdown({ reason, force, joiners }: WillShutdownEvent): void { + + // Delay so that the dialog only appears after timeout + const shutdownDialogScheduler = new RunOnceScheduler(() => { + const pendingJoiners = joiners(); + + this.progressService.withProgress({ + location: ProgressLocation.Dialog, // use a dialog to prevent the user from making any more interactions now + buttons: [this.toForceShutdownLabel(reason)], // allow to force shutdown anyway + cancellable: false, // do not allow to cancel + sticky: true, // do not allow to dismiss + title: this.toShutdownLabel(reason, false), + detail: pendingJoiners.length > 0 ? localize('willShutdownDetail', "The following operations are still running: \n{0}", pendingJoiners.map(joiner => `- ${joiner.label}`).join('\n')) : undefined + }, () => { + return Event.toPromise(this.lifecycleService.onDidShutdown); // dismiss this dialog when we actually shutdown + }, () => { + force(); + }); + }, 1200); + shutdownDialogScheduler.schedule(); + + // Dispose scheduler when we actually shutdown + Event.once(this.lifecycleService.onDidShutdown)(() => shutdownDialogScheduler.dispose()); } private toShutdownLabel(reason: ShutdownReason, isError: boolean): string { @@ -518,17 +597,17 @@ export class NativeWindow extends Disposable { pathOffset++; // for segments which are not the file name we want to open the folder } - const path = segments.slice(0, pathOffset).join(posix.sep); + const path = URI.file(segments.slice(0, pathOffset).join(posix.sep)); let label: string; if (!isFile) { - label = getBaseLabel(dirname(path)); + label = this.labelService.getUriBasenameLabel(dirname(path)); } else { - label = getBaseLabel(path); + label = this.labelService.getUriBasenameLabel(path); } const commandId = `workbench.action.revealPathInFinder${i}`; - this.customTitleContextMenuDisposable.add(CommandsRegistry.registerCommand(commandId, () => this.nativeHostService.showItemInFolder(path))); + this.customTitleContextMenuDisposable.add(CommandsRegistry.registerCommand(commandId, () => this.nativeHostService.showItemInFolder(path.fsPath))); this.customTitleContextMenuDisposable.add(MenuRegistry.appendMenuItem(MenuId.TitleBarContext, { command: { id: commandId, title: label || posix.sep }, order: -i })); } } @@ -573,27 +652,18 @@ export class NativeWindow extends Disposable { } // Smoke Test Driver - this.setupDriver(); + if (this.environmentService.enableSmokeTestDriver) { + this.setupDriver(); + } } private setupDriver(): void { - - // Browser Driver - if (this.environmentService.args['enable-smoke-test-driver']) { - const that = this; - registerWindowDriver({ - async exitApplication(): Promise<number> { - that.nativeHostService.quit(); - - return that.environmentService.mainPid; - } - }); - } - - // Legacy Driver (TODO@bpasero remove me eventually) - else if (this.environmentService.args.driver) { - this.instantiationService.invokeFunction(async accessor => this._register(await registerLegacyWindowDriver(accessor, this.nativeHostService.windowId))); - } + const that = this; + registerWindowDriver({ + async exitApplication(): Promise<void> { + return that.nativeHostService.quit(); + } + }); } private setupOpenHandlers(): void { @@ -744,9 +814,9 @@ export class NativeWindow extends Disposable { private doAddFolders(): void { const foldersToAdd: IWorkspaceFolderCreationData[] = []; - this.pendingFoldersToAdd.forEach(folder => { + for (const folder of this.pendingFoldersToAdd) { foldersToAdd.push(({ uri: folder })); - }); + } this.pendingFoldersToAdd = []; diff --git a/src/vs/workbench/services/assignment/common/assignmentService.ts b/src/vs/workbench/services/assignment/common/assignmentService.ts index d04be1ca7ed..455a4faab6b 100644 --- a/src/vs/workbench/services/assignment/common/assignmentService.ts +++ b/src/vs/workbench/services/assignment/common/assignmentService.ts @@ -67,7 +67,9 @@ class WorkbenchAssignmentServiceTelemetry implements IExperimentationTelemetry { /* __GDPR__ "query-expfeature" : { - "ABExp.queriedFeature": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + "owner": "sbatten", + "comment": "Logs queries to the experiment service by feature for metric calculations", + "ABExp.queriedFeature": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The experimental feature being queried" } } */ this.telemetryService.publicLog(eventName, data); @@ -103,8 +105,10 @@ export class WorkbenchAssignmentService extends BaseAssignmentService { }; type TASClientReadTreatmentClassification = { - treatmentValue: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; - treatmentName: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; + owner: 'sbatten'; + comment: 'Logged when a treatment value is read from the experiment service'; + treatmentValue: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The value of the read treatment' }; + treatmentName: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The name of the treatment that was read' }; }; this.telemetryService.publicLog2<TASClientReadTreatmentData, TASClientReadTreatmentClassification>('tasClientReadTreatmentComplete', diff --git a/src/vs/workbench/services/authentication/browser/authenticationService.ts b/src/vs/workbench/services/authentication/browser/authenticationService.ts index 5f7431dafe9..f543021d924 100644 --- a/src/vs/workbench/services/authentication/browser/authenticationService.ts +++ b/src/vs/workbench/services/authentication/browser/authenticationService.ts @@ -200,7 +200,7 @@ export class AuthenticationService extends Disposable implements IAuthentication this._placeholderMenuItem = MenuRegistry.appendMenuItem(MenuId.AccountsContext, { command: { id: 'noAuthenticationProviders', - title: nls.localize('loading', "Loading..."), + title: nls.localize('authentication.Placeholder', "No accounts requested yet..."), precondition: ContextKeyExpr.false() }, }); diff --git a/src/vs/workbench/services/configuration/common/configurationEditingService.ts b/src/vs/workbench/services/configuration/common/configurationEditingService.ts index 771c51dbe48..3e4e805c231 100644 --- a/src/vs/workbench/services/configuration/common/configurationEditingService.ts +++ b/src/vs/workbench/services/configuration/common/configurationEditingService.ts @@ -366,7 +366,7 @@ export class ConfigurationEditingService { case ConfigurationEditingErrorCode.ERROR_INVALID_USER_TARGET: return nls.localize('errorInvalidUserTarget', "Unable to write to User Settings because {0} does not support for global scope.", operation.key); case ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_TARGET: return nls.localize('errorInvalidWorkspaceTarget', "Unable to write to Workspace Settings because {0} does not support for workspace scope in a multi folder workspace.", operation.key); case ConfigurationEditingErrorCode.ERROR_INVALID_FOLDER_TARGET: return nls.localize('errorInvalidFolderTarget', "Unable to write to Folder Settings because no resource is provided."); - case ConfigurationEditingErrorCode.ERROR_INVALID_RESOURCE_LANGUAGE_CONFIGURATION: return nls.localize('errorInvalidResourceLanguageConfiguraiton', "Unable to write to Language Settings because {0} is not a resource language setting.", operation.key); + case ConfigurationEditingErrorCode.ERROR_INVALID_RESOURCE_LANGUAGE_CONFIGURATION: return nls.localize('errorInvalidResourceLanguageConfiguration', "Unable to write to Language Settings because {0} is not a resource language setting.", operation.key); case ConfigurationEditingErrorCode.ERROR_NO_WORKSPACE_OPENED: return nls.localize('errorNoWorkspaceOpened', "Unable to write to {0} because no workspace is opened. Please open a workspace first and try again.", this.stringifyTarget(target)); // User issues diff --git a/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts b/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts index 61cc2fc1b51..db9b1eeff33 100644 --- a/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts +++ b/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts @@ -105,6 +105,47 @@ suite('WorkspaceContextService - Folder', () => { assert.strictEqual(actual, testObject.getWorkspace().folders[0]); }); + test('getWorkspaceFolder() - queries in workspace folder', async () => { + + const logService = new NullLogService(); + const fileService = disposables.add(new FileService(logService)); + const fileSystemProvider = disposables.add(new InMemoryFileSystemProvider()); + fileService.registerProvider(ROOT.scheme, fileSystemProvider); + + const folder = joinPath(ROOT, folderName).with({ query: 'myquery=1' }); + await fileService.createFolder(folder); + + const environmentService = TestEnvironmentService; + fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService()))); + const testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, fileService, new RemoteAgentService(null, environmentService, TestProductService, new RemoteAuthorityResolverService(undefined, undefined), new SignService(undefined), new NullLogService()), new UriIdentityService(fileService), new NullLogService())); + await (<WorkspaceService>testObject).initialize(convertToWorkspacePayload(folder)); + + const actual = testObject.getWorkspaceFolder(joinPath(folder, 'a')); + + assert.strictEqual(actual, testObject.getWorkspace().folders[0]); + }); + + test('getWorkspaceFolder() - queries in resource', async () => { + + const logService = new NullLogService(); + const fileService = disposables.add(new FileService(logService)); + const fileSystemProvider = disposables.add(new InMemoryFileSystemProvider()); + fileService.registerProvider(ROOT.scheme, fileSystemProvider); + + const folder = joinPath(ROOT, folderName); + await fileService.createFolder(folder); + + const environmentService = TestEnvironmentService; + fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService()))); + const testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, fileService, new RemoteAgentService(null, environmentService, TestProductService, new RemoteAuthorityResolverService(undefined, undefined), new SignService(undefined), new NullLogService()), new UriIdentityService(fileService), new NullLogService())); + await (<WorkspaceService>testObject).initialize(convertToWorkspacePayload(folder)); + + + const actual = testObject.getWorkspaceFolder(joinPath(folder, 'a').with({ query: 'myquery=1' })); + + assert.strictEqual(actual, testObject.getWorkspace().folders[0]); + }); + test('isCurrentWorkspace() => true', () => { assert.ok(testObject.isCurrentWorkspace(folder)); }); diff --git a/src/vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService.ts b/src/vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService.ts new file mode 100644 index 00000000000..b4f3c050e7e --- /dev/null +++ b/src/vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService.ts @@ -0,0 +1,374 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { URI as uri } from 'vs/base/common/uri'; +import * as nls from 'vs/nls'; +import * as Types from 'vs/base/common/types'; +import { Schemas } from 'vs/base/common/network'; +import { SideBySideEditor, EditorResourceAccessor } from 'vs/workbench/common/editor'; +import { IStringDictionary, forEach, fromMap } from 'vs/base/common/collections'; +import { IConfigurationService, IConfigurationOverrides, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IWorkspaceFolder, IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { AbstractVariableResolverService } from 'vs/workbench/services/configurationResolver/common/variableResolver'; +import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; +import { IQuickInputService, IInputOptions, IQuickPickItem, IPickOptions } from 'vs/platform/quickinput/common/quickInput'; +import { ConfiguredInput } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; +import { IProcessEnvironment } from 'vs/base/common/platform'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { IPathService } from 'vs/workbench/services/path/common/pathService'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; + +export abstract class BaseConfigurationResolverService extends AbstractVariableResolverService { + + static readonly INPUT_OR_COMMAND_VARIABLES_PATTERN = /\${((input|command):(.*?))}/g; + + constructor( + context: { + getAppRoot: () => string | undefined; + getExecPath: () => string | undefined; + }, + envVariablesPromise: Promise<IProcessEnvironment>, + editorService: IEditorService, + private readonly configurationService: IConfigurationService, + private readonly commandService: ICommandService, + private readonly workspaceContextService: IWorkspaceContextService, + private readonly quickInputService: IQuickInputService, + private readonly labelService: ILabelService, + private readonly pathService: IPathService, + extensionService: IExtensionService, + ) { + super({ + getFolderUri: (folderName: string): uri | undefined => { + const folder = workspaceContextService.getWorkspace().folders.filter(f => f.name === folderName).pop(); + return folder ? folder.uri : undefined; + }, + getWorkspaceFolderCount: (): number => { + return workspaceContextService.getWorkspace().folders.length; + }, + getConfigurationValue: (folderUri: uri | undefined, suffix: string): string | undefined => { + return configurationService.getValue<string>(suffix, folderUri ? { resource: folderUri } : {}); + }, + getAppRoot: (): string | undefined => { + return context.getAppRoot(); + }, + getExecPath: (): string | undefined => { + return context.getExecPath(); + }, + getFilePath: (): string | undefined => { + const fileResource = EditorResourceAccessor.getOriginalUri(editorService.activeEditor, { + supportSideBySide: SideBySideEditor.PRIMARY, + filterByScheme: [Schemas.file, Schemas.vscodeUserData, this.pathService.defaultUriScheme] + }); + if (!fileResource) { + return undefined; + } + return this.labelService.getUriLabel(fileResource, { noPrefix: true }); + }, + getWorkspaceFolderPathForFile: (): string | undefined => { + const fileResource = EditorResourceAccessor.getOriginalUri(editorService.activeEditor, { + supportSideBySide: SideBySideEditor.PRIMARY, + filterByScheme: [Schemas.file, Schemas.vscodeUserData, this.pathService.defaultUriScheme] + }); + if (!fileResource) { + return undefined; + } + const wsFolder = workspaceContextService.getWorkspaceFolder(fileResource); + if (!wsFolder) { + return undefined; + } + return this.labelService.getUriLabel(wsFolder.uri, { noPrefix: true }); + }, + getSelectedText: (): string | undefined => { + const activeTextEditorControl = editorService.activeTextEditorControl; + + let activeControl: ICodeEditor | null = null; + + if (isCodeEditor(activeTextEditorControl)) { + activeControl = activeTextEditorControl; + } else if (isDiffEditor(activeTextEditorControl)) { + const original = activeTextEditorControl.getOriginalEditor(); + const modified = activeTextEditorControl.getModifiedEditor(); + activeControl = original.hasWidgetFocus() ? original : modified; + } + + const activeModel = activeControl?.getModel(); + const activeSelection = activeControl?.getSelection(); + if (activeModel && activeSelection) { + return activeModel.getValueInRange(activeSelection); + } + return undefined; + }, + getLineNumber: (): string | undefined => { + const activeTextEditorControl = editorService.activeTextEditorControl; + if (isCodeEditor(activeTextEditorControl)) { + const selection = activeTextEditorControl.getSelection(); + if (selection) { + const lineNumber = selection.positionLineNumber; + return String(lineNumber); + } + } + return undefined; + }, + getExtension: id => { + return extensionService.getExtension(id); + }, + }, labelService, pathService.userHome().then(home => home.path), envVariablesPromise); + } + + public override async resolveWithInteractionReplace(folder: IWorkspaceFolder | undefined, config: any, section?: string, variables?: IStringDictionary<string>, target?: ConfigurationTarget): Promise<any> { + // resolve any non-interactive variables and any contributed variables + config = await this.resolveAnyAsync(folder, config); + + // resolve input variables in the order in which they are encountered + return this.resolveWithInteraction(folder, config, section, variables, target).then(mapping => { + // finally substitute evaluated command variables (if there are any) + if (!mapping) { + return null; + } else if (mapping.size > 0) { + return this.resolveAnyAsync(folder, config, fromMap(mapping)); + } else { + return config; + } + }); + } + + public override async resolveWithInteraction(folder: IWorkspaceFolder | undefined, config: any, section?: string, variables?: IStringDictionary<string>, target?: ConfigurationTarget): Promise<Map<string, string> | undefined> { + // resolve any non-interactive variables and any contributed variables + const resolved = await this.resolveAnyMap(folder, config); + config = resolved.newConfig; + const allVariableMapping: Map<string, string> = resolved.resolvedVariables; + + // resolve input and command variables in the order in which they are encountered + return this.resolveWithInputAndCommands(folder, config, variables, section, target).then(inputOrCommandMapping => { + if (this.updateMapping(inputOrCommandMapping, allVariableMapping)) { + return allVariableMapping; + } + return undefined; + }); + } + + /** + * Add all items from newMapping to fullMapping. Returns false if newMapping is undefined. + */ + private updateMapping(newMapping: IStringDictionary<string> | undefined, fullMapping: Map<string, string>): boolean { + if (!newMapping) { + return false; + } + forEach(newMapping, (entry) => { + fullMapping.set(entry.key, entry.value); + }); + return true; + } + + /** + * Finds and executes all input and command variables in the given configuration and returns their values as a dictionary. + * Please note: this method does not substitute the input or command variables (so the configuration is not modified). + * The returned dictionary can be passed to "resolvePlatform" for the actual substitution. + * See #6569. + * + * @param variableToCommandMap Aliases for commands + */ + private async resolveWithInputAndCommands(folder: IWorkspaceFolder | undefined, configuration: any, variableToCommandMap?: IStringDictionary<string>, section?: string, target?: ConfigurationTarget): Promise<IStringDictionary<string> | undefined> { + + if (!configuration) { + return Promise.resolve(undefined); + } + + // get all "inputs" + let inputs: ConfiguredInput[] = []; + if (this.workspaceContextService.getWorkbenchState() !== WorkbenchState.EMPTY && section) { + const overrides: IConfigurationOverrides = folder ? { resource: folder.uri } : {}; + let result = this.configurationService.inspect(section, overrides); + if (result && (result.userValue || result.workspaceValue || result.workspaceFolderValue)) { + switch (target) { + case ConfigurationTarget.USER: inputs = (<any>result.userValue)?.inputs; break; + case ConfigurationTarget.WORKSPACE: inputs = (<any>result.workspaceValue)?.inputs; break; + default: inputs = (<any>result.workspaceFolderValue)?.inputs; + } + } else { + const valueResult = this.configurationService.getValue<any>(section, overrides); + if (valueResult) { + inputs = valueResult.inputs; + } + } + } + + // extract and dedupe all "input" and "command" variables and preserve their order in an array + const variables: string[] = []; + this.findVariables(configuration, variables); + + const variableValues: IStringDictionary<string> = Object.create(null); + + for (const variable of variables) { + + const [type, name] = variable.split(':', 2); + + let result: string | undefined; + + switch (type) { + + case 'input': + result = await this.showUserInput(name, inputs); + break; + + case 'command': { + // use the name as a command ID #12735 + const commandId = (variableToCommandMap ? variableToCommandMap[name] : undefined) || name; + result = await this.commandService.executeCommand(commandId, configuration); + if (typeof result !== 'string' && !Types.isUndefinedOrNull(result)) { + throw new Error(nls.localize('commandVariable.noStringType', "Cannot substitute command variable '{0}' because command did not return a result of type string.", commandId)); + } + break; + } + default: + // Try to resolve it as a contributed variable + if (this._contributedVariables.has(variable)) { + result = await this._contributedVariables.get(variable)!(); + } + } + + if (typeof result === 'string') { + variableValues[variable] = result; + } else { + return undefined; + } + } + + return variableValues; + } + + /** + * Recursively finds all command or input variables in object and pushes them into variables. + * @param object object is searched for variables. + * @param variables All found variables are returned in variables. + */ + private findVariables(object: any, variables: string[]) { + if (typeof object === 'string') { + let matches; + while ((matches = BaseConfigurationResolverService.INPUT_OR_COMMAND_VARIABLES_PATTERN.exec(object)) !== null) { + if (matches.length === 4) { + const command = matches[1]; + if (variables.indexOf(command) < 0) { + variables.push(command); + } + } + } + this._contributedVariables.forEach((value, contributed: string) => { + if ((variables.indexOf(contributed) < 0) && (object.indexOf('${' + contributed + '}') >= 0)) { + variables.push(contributed); + } + }); + } else if (Types.isArray(object)) { + object.forEach(value => { + this.findVariables(value, variables); + }); + } else if (object) { + Object.keys(object).forEach(key => { + const value = object[key]; + this.findVariables(value, variables); + }); + } + } + + /** + * Takes the provided input info and shows the quick pick so the user can provide the value for the input + * @param variable Name of the input variable. + * @param inputInfos Information about each possible input variable. + */ + private showUserInput(variable: string, inputInfos: ConfiguredInput[]): Promise<string | undefined> { + + if (!inputInfos) { + return Promise.reject(new Error(nls.localize('inputVariable.noInputSection', "Variable '{0}' must be defined in an '{1}' section of the debug or task configuration.", variable, 'input'))); + } + + // find info for the given input variable + const info = inputInfos.filter(item => item.id === variable).pop(); + if (info) { + + const missingAttribute = (attrName: string) => { + throw new Error(nls.localize('inputVariable.missingAttribute', "Input variable '{0}' is of type '{1}' and must include '{2}'.", variable, info.type, attrName)); + }; + + switch (info.type) { + + case 'promptString': { + if (!Types.isString(info.description)) { + missingAttribute('description'); + } + const inputOptions: IInputOptions = { prompt: info.description, ignoreFocusLost: true }; + if (info.default) { + inputOptions.value = info.default; + } + if (info.password) { + inputOptions.password = info.password; + } + return this.quickInputService.input(inputOptions).then(resolvedInput => { + return resolvedInput; + }); + } + + case 'pickString': { + if (!Types.isString(info.description)) { + missingAttribute('description'); + } + if (Types.isArray(info.options)) { + info.options.forEach(pickOption => { + if (!Types.isString(pickOption) && !Types.isString(pickOption.value)) { + missingAttribute('value'); + } + }); + } else { + missingAttribute('options'); + } + interface PickStringItem extends IQuickPickItem { + value: string; + } + const picks = new Array<PickStringItem>(); + info.options.forEach(pickOption => { + const value = Types.isString(pickOption) ? pickOption : pickOption.value; + const label = Types.isString(pickOption) ? undefined : pickOption.label; + + // If there is no label defined, use value as label + const item: PickStringItem = { + label: label ? `${label}: ${value}` : value, + value: value + }; + + if (value === info.default) { + item.description = nls.localize('inputVariable.defaultInputValue', "(Default)"); + picks.unshift(item); + } else { + picks.push(item); + } + }); + const pickOptions: IPickOptions<PickStringItem> = { placeHolder: info.description, matchOnDetail: true, ignoreFocusLost: true }; + return this.quickInputService.pick(picks, pickOptions, undefined).then(resolvedInput => { + if (resolvedInput) { + return resolvedInput.value; + } + return undefined; + }); + } + + case 'command': { + if (!Types.isString(info.command)) { + missingAttribute('command'); + } + return this.commandService.executeCommand<string>(info.command, info.args).then(result => { + if (typeof result === 'string' || Types.isUndefinedOrNull(result)) { + return result; + } + throw new Error(nls.localize('inputVariable.command.noStringType', "Cannot substitute input variable '{0}' because command '{1}' did not return a result of type string.", variable, info.command)); + }); + } + + default: + throw new Error(nls.localize('inputVariable.unknownType', "Input variable '{0}' can only be of type 'promptString', 'pickString', or 'command'.", variable)); + } + } + return Promise.reject(new Error(nls.localize('inputVariable.undefinedVariable', "Undefined input variable '{0}' encountered. Remove or define '{0}' to continue.", variable))); + } +} diff --git a/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts b/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts index acbac82cf51..4a7d00a5b4d 100644 --- a/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts +++ b/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts @@ -3,372 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URI as uri } from 'vs/base/common/uri'; -import * as nls from 'vs/nls'; -import * as Types from 'vs/base/common/types'; -import { Schemas } from 'vs/base/common/network'; -import { SideBySideEditor, EditorResourceAccessor } from 'vs/workbench/common/editor'; -import { IStringDictionary, forEach, fromMap } from 'vs/base/common/collections'; -import { IConfigurationService, IConfigurationOverrides, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IWorkspaceFolder, IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { AbstractVariableResolverService } from 'vs/workbench/services/configurationResolver/common/variableResolver'; -import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; -import { IQuickInputService, IInputOptions, IQuickPickItem, IPickOptions } from 'vs/platform/quickinput/common/quickInput'; -import { ConfiguredInput } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; -import { IProcessEnvironment } from 'vs/base/common/platform'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ILabelService } from 'vs/platform/label/common/label'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { BaseConfigurationResolverService } from 'vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService'; +import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; -export abstract class BaseConfigurationResolverService extends AbstractVariableResolverService { - - static readonly INPUT_OR_COMMAND_VARIABLES_PATTERN = /\${((input|command):(.*?))}/g; - - constructor( - context: { - getAppRoot: () => string | undefined; - getExecPath: () => string | undefined; - }, - envVariablesPromise: Promise<IProcessEnvironment>, - editorService: IEditorService, - private readonly configurationService: IConfigurationService, - private readonly commandService: ICommandService, - private readonly workspaceContextService: IWorkspaceContextService, - private readonly quickInputService: IQuickInputService, - private readonly labelService: ILabelService, - private readonly pathService: IPathService - ) { - super({ - getFolderUri: (folderName: string): uri | undefined => { - const folder = workspaceContextService.getWorkspace().folders.filter(f => f.name === folderName).pop(); - return folder ? folder.uri : undefined; - }, - getWorkspaceFolderCount: (): number => { - return workspaceContextService.getWorkspace().folders.length; - }, - getConfigurationValue: (folderUri: uri | undefined, suffix: string): string | undefined => { - return configurationService.getValue<string>(suffix, folderUri ? { resource: folderUri } : {}); - }, - getAppRoot: (): string | undefined => { - return context.getAppRoot(); - }, - getExecPath: (): string | undefined => { - return context.getExecPath(); - }, - getFilePath: (): string | undefined => { - const fileResource = EditorResourceAccessor.getOriginalUri(editorService.activeEditor, { - supportSideBySide: SideBySideEditor.PRIMARY, - filterByScheme: [Schemas.file, Schemas.vscodeUserData, this.pathService.defaultUriScheme] - }); - if (!fileResource) { - return undefined; - } - return this.labelService.getUriLabel(fileResource, { noPrefix: true }); - }, - getWorkspaceFolderPathForFile: (): string | undefined => { - const fileResource = EditorResourceAccessor.getOriginalUri(editorService.activeEditor, { - supportSideBySide: SideBySideEditor.PRIMARY, - filterByScheme: [Schemas.file, Schemas.vscodeUserData, this.pathService.defaultUriScheme] - }); - if (!fileResource) { - return undefined; - } - const wsFolder = workspaceContextService.getWorkspaceFolder(fileResource); - if (!wsFolder) { - return undefined; - } - return this.labelService.getUriLabel(wsFolder.uri, { noPrefix: true }); - }, - getSelectedText: (): string | undefined => { - const activeTextEditorControl = editorService.activeTextEditorControl; - - let activeControl: ICodeEditor | null = null; - - if (isCodeEditor(activeTextEditorControl)) { - activeControl = activeTextEditorControl; - } else if (isDiffEditor(activeTextEditorControl)) { - const original = activeTextEditorControl.getOriginalEditor(); - const modified = activeTextEditorControl.getModifiedEditor(); - activeControl = original.hasWidgetFocus() ? original : modified; - } - - const activeModel = activeControl?.getModel(); - const activeSelection = activeControl?.getSelection(); - if (activeModel && activeSelection) { - return activeModel.getValueInRange(activeSelection); - } - return undefined; - }, - getLineNumber: (): string | undefined => { - const activeTextEditorControl = editorService.activeTextEditorControl; - if (isCodeEditor(activeTextEditorControl)) { - const selection = activeTextEditorControl.getSelection(); - if (selection) { - const lineNumber = selection.positionLineNumber; - return String(lineNumber); - } - } - return undefined; - } - }, labelService, pathService.userHome().then(home => home.path), envVariablesPromise); - } - - public override async resolveWithInteractionReplace(folder: IWorkspaceFolder | undefined, config: any, section?: string, variables?: IStringDictionary<string>, target?: ConfigurationTarget): Promise<any> { - // resolve any non-interactive variables and any contributed variables - config = await this.resolveAnyAsync(folder, config); - - // resolve input variables in the order in which they are encountered - return this.resolveWithInteraction(folder, config, section, variables, target).then(mapping => { - // finally substitute evaluated command variables (if there are any) - if (!mapping) { - return null; - } else if (mapping.size > 0) { - return this.resolveAnyAsync(folder, config, fromMap(mapping)); - } else { - return config; - } - }); - } - - public override async resolveWithInteraction(folder: IWorkspaceFolder | undefined, config: any, section?: string, variables?: IStringDictionary<string>, target?: ConfigurationTarget): Promise<Map<string, string> | undefined> { - // resolve any non-interactive variables and any contributed variables - const resolved = await this.resolveAnyMap(folder, config); - config = resolved.newConfig; - const allVariableMapping: Map<string, string> = resolved.resolvedVariables; - - // resolve input and command variables in the order in which they are encountered - return this.resolveWithInputAndCommands(folder, config, variables, section, target).then(inputOrCommandMapping => { - if (this.updateMapping(inputOrCommandMapping, allVariableMapping)) { - return allVariableMapping; - } - return undefined; - }); - } - - /** - * Add all items from newMapping to fullMapping. Returns false if newMapping is undefined. - */ - private updateMapping(newMapping: IStringDictionary<string> | undefined, fullMapping: Map<string, string>): boolean { - if (!newMapping) { - return false; - } - forEach(newMapping, (entry) => { - fullMapping.set(entry.key, entry.value); - }); - return true; - } - - /** - * Finds and executes all input and command variables in the given configuration and returns their values as a dictionary. - * Please note: this method does not substitute the input or command variables (so the configuration is not modified). - * The returned dictionary can be passed to "resolvePlatform" for the actual substitution. - * See #6569. - * - * @param variableToCommandMap Aliases for commands - */ - private async resolveWithInputAndCommands(folder: IWorkspaceFolder | undefined, configuration: any, variableToCommandMap?: IStringDictionary<string>, section?: string, target?: ConfigurationTarget): Promise<IStringDictionary<string> | undefined> { - - if (!configuration) { - return Promise.resolve(undefined); - } - - // get all "inputs" - let inputs: ConfiguredInput[] = []; - if (this.workspaceContextService.getWorkbenchState() !== WorkbenchState.EMPTY && section) { - const overrides: IConfigurationOverrides = folder ? { resource: folder.uri } : {}; - let result = this.configurationService.inspect(section, overrides); - if (result && (result.userValue || result.workspaceValue || result.workspaceFolderValue)) { - switch (target) { - case ConfigurationTarget.USER: inputs = (<any>result.userValue)?.inputs; break; - case ConfigurationTarget.WORKSPACE: inputs = (<any>result.workspaceValue)?.inputs; break; - default: inputs = (<any>result.workspaceFolderValue)?.inputs; - } - } else { - const valueResult = this.configurationService.getValue<any>(section, overrides); - if (valueResult) { - inputs = valueResult.inputs; - } - } - } - - // extract and dedupe all "input" and "command" variables and preserve their order in an array - const variables: string[] = []; - this.findVariables(configuration, variables); - - const variableValues: IStringDictionary<string> = Object.create(null); - - for (const variable of variables) { - - const [type, name] = variable.split(':', 2); - - let result: string | undefined; - - switch (type) { - - case 'input': - result = await this.showUserInput(name, inputs); - break; - - case 'command': { - // use the name as a command ID #12735 - const commandId = (variableToCommandMap ? variableToCommandMap[name] : undefined) || name; - result = await this.commandService.executeCommand(commandId, configuration); - if (typeof result !== 'string' && !Types.isUndefinedOrNull(result)) { - throw new Error(nls.localize('commandVariable.noStringType', "Cannot substitute command variable '{0}' because command did not return a result of type string.", commandId)); - } - break; - } - default: - // Try to resolve it as a contributed variable - if (this._contributedVariables.has(variable)) { - result = await this._contributedVariables.get(variable)!(); - } - } - - if (typeof result === 'string') { - variableValues[variable] = result; - } else { - return undefined; - } - } - - return variableValues; - } - - /** - * Recursively finds all command or input variables in object and pushes them into variables. - * @param object object is searched for variables. - * @param variables All found variables are returned in variables. - */ - private findVariables(object: any, variables: string[]) { - if (typeof object === 'string') { - let matches; - while ((matches = BaseConfigurationResolverService.INPUT_OR_COMMAND_VARIABLES_PATTERN.exec(object)) !== null) { - if (matches.length === 4) { - const command = matches[1]; - if (variables.indexOf(command) < 0) { - variables.push(command); - } - } - } - this._contributedVariables.forEach((value, contributed: string) => { - if ((variables.indexOf(contributed) < 0) && (object.indexOf('${' + contributed + '}') >= 0)) { - variables.push(contributed); - } - }); - } else if (Types.isArray(object)) { - object.forEach(value => { - this.findVariables(value, variables); - }); - } else if (object) { - Object.keys(object).forEach(key => { - const value = object[key]; - this.findVariables(value, variables); - }); - } - } - - /** - * Takes the provided input info and shows the quick pick so the user can provide the value for the input - * @param variable Name of the input variable. - * @param inputInfos Information about each possible input variable. - */ - private showUserInput(variable: string, inputInfos: ConfiguredInput[]): Promise<string | undefined> { - - if (!inputInfos) { - return Promise.reject(new Error(nls.localize('inputVariable.noInputSection', "Variable '{0}' must be defined in an '{1}' section of the debug or task configuration.", variable, 'input'))); - } - - // find info for the given input variable - const info = inputInfos.filter(item => item.id === variable).pop(); - if (info) { - - const missingAttribute = (attrName: string) => { - throw new Error(nls.localize('inputVariable.missingAttribute', "Input variable '{0}' is of type '{1}' and must include '{2}'.", variable, info.type, attrName)); - }; - - switch (info.type) { - - case 'promptString': { - if (!Types.isString(info.description)) { - missingAttribute('description'); - } - const inputOptions: IInputOptions = { prompt: info.description, ignoreFocusLost: true }; - if (info.default) { - inputOptions.value = info.default; - } - if (info.password) { - inputOptions.password = info.password; - } - return this.quickInputService.input(inputOptions).then(resolvedInput => { - return resolvedInput; - }); - } - - case 'pickString': { - if (!Types.isString(info.description)) { - missingAttribute('description'); - } - if (Types.isArray(info.options)) { - info.options.forEach(pickOption => { - if (!Types.isString(pickOption) && !Types.isString(pickOption.value)) { - missingAttribute('value'); - } - }); - } else { - missingAttribute('options'); - } - interface PickStringItem extends IQuickPickItem { - value: string; - } - const picks = new Array<PickStringItem>(); - info.options.forEach(pickOption => { - const value = Types.isString(pickOption) ? pickOption : pickOption.value; - const label = Types.isString(pickOption) ? undefined : pickOption.label; - - // If there is no label defined, use value as label - const item: PickStringItem = { - label: label ? `${label}: ${value}` : value, - value: value - }; - - if (value === info.default) { - item.description = nls.localize('inputVariable.defaultInputValue', "(Default)"); - picks.unshift(item); - } else { - picks.push(item); - } - }); - const pickOptions: IPickOptions<PickStringItem> = { placeHolder: info.description, matchOnDetail: true, ignoreFocusLost: true }; - return this.quickInputService.pick(picks, pickOptions, undefined).then(resolvedInput => { - if (resolvedInput) { - return resolvedInput.value; - } - return undefined; - }); - } - - case 'command': { - if (!Types.isString(info.command)) { - missingAttribute('command'); - } - return this.commandService.executeCommand<string>(info.command, info.args).then(result => { - if (typeof result === 'string' || Types.isUndefinedOrNull(result)) { - return result; - } - throw new Error(nls.localize('inputVariable.command.noStringType', "Cannot substitute input variable '{0}' because command '{1}' did not return a result of type string.", variable, info.command)); - }); - } - - default: - throw new Error(nls.localize('inputVariable.unknownType', "Input variable '{0}' can only be of type 'promptString', 'pickString', or 'command'.", variable)); - } - } - return Promise.reject(new Error(nls.localize('inputVariable.undefinedVariable', "Undefined input variable '{0}' encountered. Remove or define '{0}' to continue.", variable))); - } -} - export class ConfigurationResolverService extends BaseConfigurationResolverService { constructor( @@ -378,10 +24,13 @@ export class ConfigurationResolverService extends BaseConfigurationResolverServi @IWorkspaceContextService workspaceContextService: IWorkspaceContextService, @IQuickInputService quickInputService: IQuickInputService, @ILabelService labelService: ILabelService, - @IPathService pathService: IPathService + @IPathService pathService: IPathService, + @IExtensionService extensionService: IExtensionService, ) { super({ getAppRoot: () => undefined, getExecPath: () => undefined }, Promise.resolve(Object.create(null)), editorService, configurationService, - commandService, workspaceContextService, quickInputService, labelService, pathService); + commandService, workspaceContextService, quickInputService, labelService, pathService, extensionService); } } + +registerSingleton(IConfigurationResolverService, ConfigurationResolverService, true); diff --git a/src/vs/workbench/services/configurationResolver/common/configurationResolver.ts b/src/vs/workbench/services/configurationResolver/common/configurationResolver.ts index d8ba161425b..37a44d15196 100644 --- a/src/vs/workbench/services/configurationResolver/common/configurationResolver.ts +++ b/src/vs/workbench/services/configurationResolver/common/configurationResolver.ts @@ -14,7 +14,7 @@ export const IConfigurationResolverService = createDecorator<IConfigurationResol export interface IConfigurationResolverService { readonly _serviceBrand: undefined; - resolveWithEnvironment(environment: IProcessEnvironment, folder: IWorkspaceFolder | undefined, value: string): string; + resolveWithEnvironment(environment: IProcessEnvironment, folder: IWorkspaceFolder | undefined, value: string): Promise<string>; resolveAsync(folder: IWorkspaceFolder | undefined, value: string): Promise<string>; resolveAsync(folder: IWorkspaceFolder | undefined, value: string[]): Promise<string[]>; diff --git a/src/vs/workbench/services/configurationResolver/common/variableResolver.ts b/src/vs/workbench/services/configurationResolver/common/variableResolver.ts index a75a3c77ef7..7e557d33120 100644 --- a/src/vs/workbench/services/configurationResolver/common/variableResolver.ts +++ b/src/vs/workbench/services/configurationResolver/common/variableResolver.ts @@ -15,6 +15,7 @@ import { URI as uri } from 'vs/base/common/uri'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { ILabelService } from 'vs/platform/label/common/label'; +import { replaceAsync } from 'vs/base/common/strings'; export interface IVariableResolveContext { getFolderUri(folderName: string): uri | undefined; @@ -26,6 +27,7 @@ export interface IVariableResolveContext { getWorkspaceFolderPathForFile?(): string | undefined; getSelectedText(): string | undefined; getLineNumber(): string | undefined; + getExtension(id: string): Promise<{ readonly extensionLocation: uri } | undefined>; } type Environment = { env: IProcessEnvironment | undefined; userHome: string | undefined }; @@ -66,7 +68,7 @@ export class AbstractVariableResolverService implements IConfigurationResolverSe return envVariables; } - public resolveWithEnvironment(environment: IProcessEnvironment, root: IWorkspaceFolder | undefined, value: string): string { + public resolveWithEnvironment(environment: IProcessEnvironment, root: IWorkspaceFolder | undefined, value: string): Promise<string> { return this.recursiveResolve({ env: this.prepareEnv(environment), userHome: undefined }, root ? root.uri : undefined, value); } @@ -133,52 +135,53 @@ export class AbstractVariableResolverService implements IConfigurationResolverSe } } - private recursiveResolve(environment: Environment, folderUri: uri | undefined, value: any, commandValueMapping?: IStringDictionary<string>, resolvedVariables?: Map<string, string>): any { + private async recursiveResolve(environment: Environment, folderUri: uri | undefined, value: any, commandValueMapping?: IStringDictionary<string>, resolvedVariables?: Map<string, string>): Promise<any> { if (types.isString(value)) { return this.resolveString(environment, folderUri, value, commandValueMapping, resolvedVariables); } else if (types.isArray(value)) { - return value.map(s => this.recursiveResolve(environment, folderUri, s, commandValueMapping, resolvedVariables)); + return Promise.all(value.map(s => this.recursiveResolve(environment, folderUri, s, commandValueMapping, resolvedVariables))); } else if (types.isObject(value)) { let result: IStringDictionary<string | IStringDictionary<string> | string[]> = Object.create(null); - Object.keys(value).forEach(key => { - const replaced = this.resolveString(environment, folderUri, key, commandValueMapping, resolvedVariables); - result[replaced] = this.recursiveResolve(environment, folderUri, value[key], commandValueMapping, resolvedVariables); - }); + const replaced = await Promise.all(Object.keys(value).map(async key => { + const replaced = await this.resolveString(environment, folderUri, key, commandValueMapping, resolvedVariables); + return [replaced, await this.recursiveResolve(environment, folderUri, value[key], commandValueMapping, resolvedVariables)] as const; + })); + // two step process to preserve object key order + for (const [key, value] of replaced) { + result[key] = value; + } return result; } return value; } - private resolveString(environment: Environment, folderUri: uri | undefined, value: string, commandValueMapping: IStringDictionary<string> | undefined, resolvedVariables?: Map<string, string>): string { - + private resolveString(environment: Environment, folderUri: uri | undefined, value: string, commandValueMapping: IStringDictionary<string> | undefined, resolvedVariables?: Map<string, string>): Promise<string> { // loop through all variables occurrences in 'value' - const replaced = value.replace(AbstractVariableResolverService.VARIABLE_REGEXP, (match: string, variable: string) => { + return replaceAsync(value, AbstractVariableResolverService.VARIABLE_REGEXP, async (match: string, variable: string) => { // disallow attempted nesting, see #77289. This doesn't exclude variables that resolve to other variables. if (variable.includes(AbstractVariableResolverService.VARIABLE_LHS)) { return match; } - let resolvedValue = this.evaluateSingleVariable(environment, match, variable, folderUri, commandValueMapping); + let resolvedValue = await this.evaluateSingleVariable(environment, match, variable, folderUri, commandValueMapping); if (resolvedVariables) { resolvedVariables.set(variable, resolvedValue); } if ((resolvedValue !== match) && types.isString(resolvedValue) && resolvedValue.match(AbstractVariableResolverService.VARIABLE_REGEXP)) { - resolvedValue = this.resolveString(environment, folderUri, resolvedValue, commandValueMapping, resolvedVariables); + resolvedValue = await this.resolveString(environment, folderUri, resolvedValue, commandValueMapping, resolvedVariables); } return resolvedValue; }); - - return replaced; } private fsPath(displayUri: uri): string { return this._labelService ? this._labelService.getUriLabel(displayUri, { noPrefix: true }) : displayUri.fsPath; } - private evaluateSingleVariable(environment: Environment, match: string, variable: string, folderUri: uri | undefined, commandValueMapping: IStringDictionary<string> | undefined): string { + private async evaluateSingleVariable(environment: Environment, match: string, variable: string, folderUri: uri | undefined, commandValueMapping: IStringDictionary<string> | undefined): Promise<string> { // try to separate variable arguments from variable name let argument: string | undefined; @@ -268,6 +271,16 @@ export class AbstractVariableResolverService implements IConfigurationResolverSe case 'input': return this.resolveFromMap(match, argument, commandValueMapping, 'input'); + case 'extensionInstallFolder': + if (argument) { + const ext = await this._context.getExtension(argument); + if (!ext) { + throw new Error(localize('extensionNotInstalled', "Variable {0} can not be resolved because the extension {1} is not installed.", match, argument)); + } + return this.fsPath(ext.extensionLocation); + } + throw new Error(localize('missingExtensionName', "Variable {0} can not be resolved because no extension name is given.", match)); + default: { switch (variable) { diff --git a/src/vs/workbench/services/configurationResolver/electron-sandbox/configurationResolverService.ts b/src/vs/workbench/services/configurationResolver/electron-sandbox/configurationResolverService.ts index b98d3a68357..14a385e3e06 100644 --- a/src/vs/workbench/services/configurationResolver/electron-sandbox/configurationResolverService.ts +++ b/src/vs/workbench/services/configurationResolver/electron-sandbox/configurationResolverService.ts @@ -11,10 +11,11 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { BaseConfigurationResolverService } from 'vs/workbench/services/configurationResolver/browser/configurationResolverService'; +import { BaseConfigurationResolverService } from 'vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService'; import { ILabelService } from 'vs/platform/label/common/label'; import { IShellEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/shellEnvironmentService'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; export class ConfigurationResolverService extends BaseConfigurationResolverService { @@ -27,7 +28,8 @@ export class ConfigurationResolverService extends BaseConfigurationResolverServi @IQuickInputService quickInputService: IQuickInputService, @ILabelService labelService: ILabelService, @IShellEnvironmentService shellEnvironmentService: IShellEnvironmentService, - @IPathService pathService: IPathService + @IPathService pathService: IPathService, + @IExtensionService extensionService: IExtensionService, ) { super({ getAppRoot: (): string | undefined => { @@ -35,9 +37,9 @@ export class ConfigurationResolverService extends BaseConfigurationResolverServi }, getExecPath: (): string | undefined => { return environmentService.execPath; - } + }, }, shellEnvironmentService.getShellEnv(), editorService, configurationService, commandService, - workspaceContextService, quickInputService, labelService, pathService); + workspaceContextService, quickInputService, labelService, pathService, extensionService); } } diff --git a/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts b/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts index 014d9f42c43..330be4494f0 100644 --- a/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts +++ b/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts @@ -4,27 +4,30 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import { stub } from 'sinon'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { IPath, normalize } from 'vs/base/common/path'; import * as platform from 'vs/base/common/platform'; import { isObject } from 'vs/base/common/types'; -import { URI as uri } from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { Selection } from 'vs/editor/common/core/selection'; import { EditorType } from 'vs/editor/common/editorCommon'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { IFormatterChangeEvent, ILabelService, ResourceLabelFormatter } from 'vs/platform/label/common/label'; import { IWorkspace, IWorkspaceFolder, IWorkspaceIdentifier, Workspace } from 'vs/platform/workspace/common/workspace'; import { testWorkspace } from 'vs/platform/workspace/test/common/testWorkspace'; -import { BaseConfigurationResolverService } from 'vs/workbench/services/configurationResolver/browser/configurationResolverService'; +import { BaseConfigurationResolverService } from 'vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { NativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { TestEditorService, TestQuickInputService } from 'vs/workbench/test/browser/workbenchTestServices'; -import { TestContextService, TestProductService } from 'vs/workbench/test/common/workbenchTestServices'; +import { TestContextService, TestExtensionService, TestProductService } from 'vs/workbench/test/common/workbenchTestServices'; import { TestNativeWindowConfiguration } from 'vs/workbench/test/electron-browser/workbenchTestServices'; const mockLineNumber = 10; @@ -42,7 +45,7 @@ class TestEditorServiceWithActiveEditor extends TestEditorService { override get activeEditor(): any { return { get resource(): any { - return uri.parse('file:///VSCode/workspaceLocation/file'); + return URI.parse('file:///VSCode/workspaceLocation/file'); } }; } @@ -68,6 +71,7 @@ suite('Configuration Resolver Service', () => { let quickInputService: TestQuickInputService; let labelService: MockLabelService; let pathService: MockPathService; + let extensionService: IExtensionService; setup(() => { mockCommandService = new MockCommandService(); @@ -76,9 +80,10 @@ suite('Configuration Resolver Service', () => { environmentService = new MockWorkbenchEnvironmentService(envVariables); labelService = new MockLabelService(); pathService = new MockPathService(); - containingWorkspace = testWorkspace(uri.parse('file:///VSCode/workspaceLocation')); + extensionService = new TestExtensionService(); + containingWorkspace = testWorkspace(URI.parse('file:///VSCode/workspaceLocation')); workspace = containingWorkspace.folders[0]; - configurationResolverService = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), editorService, new MockInputsConfigurationService(), mockCommandService, new TestContextService(containingWorkspace), quickInputService, labelService, pathService); + configurationResolverService = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), editorService, new MockInputsConfigurationService(), mockCommandService, new TestContextService(containingWorkspace), quickInputService, labelService, pathService, extensionService); }); teardown(() => { @@ -185,6 +190,13 @@ suite('Configuration Resolver Service', () => { assert.strictEqual(await configurationResolverService!.resolveAsync(workspace, '${env:key1} ${env:key1${env:key2}}'), 'Value for key1 ${env:key1${env:key2}}'); }); + test('supports extensionDir', async () => { + const getExtension = stub(extensionService, 'getExtension'); + getExtension.withArgs('publisher.extId').returns(Promise.resolve({ extensionLocation: URI.file('/some/path') } as IExtensionDescription)); + + assert.strictEqual(await configurationResolverService!.resolveAsync(workspace, '${extensionInstallFolder:publisher.extId}'), URI.file('/some/path').fsPath); + }); + // test('substitute keys and values in object', () => { // const myObject = { // '${workspaceRootFolderName}': '${lineNumber}', @@ -217,7 +229,7 @@ suite('Configuration Resolver Service', () => { } }); - let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService); + let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService, extensionService); assert.strictEqual(await service.resolveAsync(workspace, 'abc ${config:editor.fontFamily} xyz'), 'abc foo xyz'); }); @@ -228,7 +240,7 @@ suite('Configuration Resolver Service', () => { } }); - let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService); + let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService, extensionService); assert.strictEqual(await service.resolveAsync(undefined, 'abc ${config:editor.fontFamily} xyz'), 'abc foo xyz'); }); @@ -245,7 +257,7 @@ suite('Configuration Resolver Service', () => { } }); - let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService); + let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService, extensionService); assert.strictEqual(await service.resolveAsync(workspace, 'abc ${config:editor.fontFamily} ${config:terminal.integrated.fontFamily} xyz'), 'abc foo bar xyz'); }); @@ -262,7 +274,7 @@ suite('Configuration Resolver Service', () => { } }); - let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService); + let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService, extensionService); if (platform.isWindows) { assert.strictEqual(await service.resolveAsync(workspace, 'abc ${config:editor.fontFamily} ${workspaceFolder} ${env:key1} xyz'), 'abc foo \\VSCode\\workspaceLocation Value for key1 xyz'); } else { @@ -283,7 +295,7 @@ suite('Configuration Resolver Service', () => { } }); - let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService); + let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService, extensionService); if (platform.isWindows) { assert.strictEqual(await service.resolveAsync(workspace, '${config:editor.fontFamily} ${config:terminal.integrated.fontFamily} ${workspaceFolder} - ${workspaceFolder} ${env:key1} - ${env:key2}'), 'foo bar \\VSCode\\workspaceLocation - \\VSCode\\workspaceLocation Value for key1 - Value for key2'); } else { @@ -317,7 +329,7 @@ suite('Configuration Resolver Service', () => { } }); - let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService); + let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService, extensionService); assert.strictEqual(await service.resolveAsync(workspace, 'abc ${config:editor.fontFamily} ${config:editor.lineNumbers} ${config:editor.insertSpaces} xyz'), 'abc foo 123 false xyz'); }); @@ -327,7 +339,7 @@ suite('Configuration Resolver Service', () => { editor: {} }); - let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService); + let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService, extensionService); assert.strictEqual(await service.resolveAsync(workspace, 'abc ${unknownVariable} xyz'), 'abc ${unknownVariable} xyz'); assert.strictEqual(await service.resolveAsync(workspace, 'abc ${env:unknownVariable} xyz'), 'abc xyz'); }); @@ -340,7 +352,7 @@ suite('Configuration Resolver Service', () => { } }); - let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService); + let service = new TestConfigurationResolverService(nullContext, Promise.resolve(environmentService.userEnv), new TestEditorServiceWithActiveEditor(), configurationService, mockCommandService, new TestContextService(), quickInputService, labelService, pathService, extensionService); assert.rejects(async () => await service.resolveAsync(workspace, 'abc ${env} xyz')); assert.rejects(async () => await service.resolveAsync(workspace, 'abc ${env:} xyz')); @@ -632,13 +644,13 @@ suite('Configuration Resolver Service', () => { }); }); - test('resolveWithEnvironment', () => { + test('resolveWithEnvironment', async () => { const env = { 'VAR_1': 'VAL_1', 'VAR_2': 'VAL_2' }; const configuration = 'echo ${env:VAR_1}${env:VAR_2}'; - const resolvedResult = configurationResolverService!.resolveWithEnvironment({ ...env }, undefined, configuration); + const resolvedResult = await configurationResolverService!.resolveWithEnvironment({ ...env }, undefined, configuration); assert.deepStrictEqual(resolvedResult, 'echo VAL_1VAL_2'); }); }); @@ -667,13 +679,13 @@ class MockCommandService implements ICommandService { class MockLabelService implements ILabelService { _serviceBrand: undefined; - getUriLabel(resource: uri, options?: { relative?: boolean | undefined; noPrefix?: boolean | undefined; endWithSeparator?: boolean | undefined }): string { + getUriLabel(resource: URI, options?: { relative?: boolean | undefined; noPrefix?: boolean | undefined }): string { return normalize(resource.fsPath); } - getUriBasenameLabel(resource: uri): string { + getUriBasenameLabel(resource: URI): string { throw new Error('Method not implemented.'); } - getWorkspaceLabel(workspace: uri | IWorkspaceIdentifier | IWorkspace, options?: { verbose: boolean }): string { + getWorkspaceLabel(workspace: URI | IWorkspaceIdentifier | IWorkspace, options?: { verbose: boolean }): string { throw new Error('Method not implemented.'); } getHostLabel(scheme: string, authority?: string): string { @@ -697,18 +709,21 @@ class MockPathService implements IPathService { throw new Error('Property not implemented'); } defaultUriScheme: string = Schemas.file; - fileURI(path: string): Promise<uri> { + fileURI(path: string): Promise<URI> { throw new Error('Method not implemented.'); } - async userHome(options?: { preferLocal: boolean }): Promise<uri> { - return uri.file('c:\\users\\username'); + userHome(options?: { preferLocal: boolean }): Promise<URI>; + userHome(options: { preferLocal: true }): URI; + userHome(options?: { preferLocal: boolean }): Promise<URI> | URI { + const uri = URI.file('c:\\users\\username'); + return options?.preferLocal ? uri : Promise.resolve(uri); } - hasValidBasename(resource: uri, basename?: string): Promise<boolean>; - hasValidBasename(resource: uri, os: platform.OperatingSystem, basename?: string): boolean; - hasValidBasename(resource: uri, arg2?: string | platform.OperatingSystem, name?: string): boolean | Promise<boolean> { + hasValidBasename(resource: URI, basename?: string): Promise<boolean>; + hasValidBasename(resource: URI, os: platform.OperatingSystem, basename?: string): boolean; + hasValidBasename(resource: URI, arg2?: string | platform.OperatingSystem, name?: string): boolean | Promise<boolean> { throw new Error('Method not implemented.'); } - resolvedUserHome: uri | undefined; + resolvedUserHome: URI | undefined; } class MockInputsConfigurationService extends TestConfigurationService { diff --git a/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts b/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts index 464e02e1d57..b491ce7079e 100644 --- a/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts +++ b/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts @@ -97,10 +97,24 @@ class NativeContextMenuService extends Disposable implements IContextMenuService let x: number; let y: number; - const zoom = getZoomFactor(); + let zoom = getZoomFactor(); if (dom.isHTMLElement(anchor)) { const elementPosition = dom.getDomNodePagePosition(anchor); + // When drawing context menus, we adjust the pixel position for native menus using zoom level + // In areas where zoom is applied to the element or its ancestors, we need to adjust accordingly + // e.g. The title bar has counter zoom behavior meaning it applies the inverse of zoom level. + // Window Zoom Level: 1.5, Title Bar Zoom: 1/1.5, Coordinate Multiplier: 1.5 * 1.0 / 1.5 = 1.0 + let testElement: HTMLElement | null = anchor; + do { + const elementZoomLevel = (dom.getComputedStyle(testElement) as any).zoom; + if (elementZoomLevel !== null && elementZoomLevel !== undefined && elementZoomLevel !== '1') { + zoom *= elementZoomLevel; + } + + testElement = testElement.parentElement; + } while (testElement !== null && testElement !== document.documentElement); + x = elementPosition.left; y = elementPosition.top + elementPosition.height; diff --git a/src/vs/workbench/services/credentials/test/browser/credentialsService.test.ts b/src/vs/workbench/services/credentials/test/browser/credentialsService.test.ts index 18ba51382bb..8d1e35b4997 100644 --- a/src/vs/workbench/services/credentials/test/browser/credentialsService.test.ts +++ b/src/vs/workbench/services/credentials/test/browser/credentialsService.test.ts @@ -6,8 +6,7 @@ import * as assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { BrowserCredentialsService } from 'vs/workbench/services/credentials/browser/credentialsService'; -import { TestRemoteAgentService } from 'vs/workbench/services/remote/test/common/testServices'; -import { TestEnvironmentService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestEnvironmentService, TestRemoteAgentService } from 'vs/workbench/test/browser/workbenchTestServices'; import { TestProductService } from 'vs/workbench/test/common/workbenchTestServices'; suite('CredentialsService - web', () => { diff --git a/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts b/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts index eb895e4de5e..1cb9e2a9af9 100644 --- a/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts +++ b/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import { IWindowOpenable, isWorkspaceToOpen, isFileToOpen } from 'vs/platform/window/common/window'; import { IPickAndOpenOptions, ISaveDialogOptions, IOpenDialogOptions, FileFilter, IFileDialogService, IDialogService, ConfirmResult, getFileNamesMessage } from 'vs/platform/dialogs/common/dialogs'; -import { isSavedWorkspace, IWorkspaceContextService, WorkbenchState, WORKSPACE_EXTENSION } from 'vs/platform/workspace/common/workspace'; +import { isSavedWorkspace, isTemporaryWorkspace, IWorkspaceContextService, WorkbenchState, WORKSPACE_EXTENSION } from 'vs/platform/workspace/common/workspace'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { URI } from 'vs/base/common/uri'; @@ -30,6 +30,7 @@ import { ICommandService } from 'vs/platform/commands/common/commands'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { EditorOpenSource } from 'vs/platform/editor/common/editor'; +import { ILogService } from 'vs/platform/log/common/log'; export abstract class AbstractFileDialogService implements IFileDialogService { @@ -51,7 +52,8 @@ export abstract class AbstractFileDialogService implements IFileDialogService { @IPathService private readonly pathService: IPathService, @ICommandService protected readonly commandService: ICommandService, @IEditorService protected readonly editorService: IEditorService, - @ICodeEditorService protected readonly codeEditorService: ICodeEditorService + @ICodeEditorService protected readonly codeEditorService: ICodeEditorService, + @ILogService private readonly logService: ILogService ) { } async defaultFilePath(schemeFilter = this.getSchemeFilterForWindow()): Promise<URI> { @@ -63,7 +65,7 @@ export abstract class AbstractFileDialogService implements IFileDialogService { if (!candidate) { candidate = this.historyService.getLastActiveWorkspaceRoot(schemeFilter); } else { - candidate = candidate && resources.dirname(candidate); + candidate = resources.dirname(candidate); } if (!candidate) { @@ -85,18 +87,19 @@ export abstract class AbstractFileDialogService implements IFileDialogService { if (!candidate) { return this.pathService.userHome({ preferLocal: schemeFilter === Schemas.file }); - } else { - return resources.dirname(candidate); } + + return resources.dirname(candidate); } - async defaultWorkspacePath(schemeFilter = this.getSchemeFilterForWindow(), filename?: string): Promise<URI> { + async defaultWorkspacePath(schemeFilter = this.getSchemeFilterForWindow()): Promise<URI> { let defaultWorkspacePath: URI | undefined; + // Check for current workspace config file first... if (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) { const configuration = this.contextService.getWorkspace().configuration; - if (configuration && configuration.scheme === schemeFilter && isSavedWorkspace(configuration, this.environmentService)) { - defaultWorkspacePath = resources.dirname(configuration) || undefined; + if (configuration?.scheme === schemeFilter && isSavedWorkspace(configuration, this.environmentService) && !isTemporaryWorkspace(configuration)) { + defaultWorkspacePath = resources.dirname(configuration); } } @@ -105,21 +108,28 @@ export abstract class AbstractFileDialogService implements IFileDialogService { defaultWorkspacePath = await this.defaultFilePath(schemeFilter); } - if (defaultWorkspacePath && filename) { - defaultWorkspacePath = resources.joinPath(defaultWorkspacePath, filename); - } - return defaultWorkspacePath; } async showSaveConfirm(fileNamesOrResources: (string | URI)[]): Promise<ConfirmResult> { - if (this.environmentService.isExtensionDevelopment && this.environmentService.extensionTestsLocationURI) { - return ConfirmResult.DONT_SAVE; // no veto when we are in extension dev testing mode because we cannot assume we run interactive + if (this.skipDialogs()) { + this.logService.trace('FileDialogService: refused to show save confirmation dialog in tests.'); + + // no veto when we are in extension dev testing mode because we cannot assume we run interactive + return ConfirmResult.DONT_SAVE; } return this.doShowSaveConfirm(fileNamesOrResources); } + private skipDialogs(): boolean { + if (this.environmentService.isExtensionDevelopment && this.environmentService.extensionTestsLocationURI) { + return true; // integration tests + } + + return !!this.environmentService.enableSmokeTestDriver; // smoke tests + } + private async doShowSaveConfirm(fileNamesOrResources: (string | URI)[]): Promise<ConfirmResult> { if (fileNamesOrResources.length === 0) { return ConfirmResult.DONT_SAVE; @@ -195,8 +205,10 @@ export abstract class AbstractFileDialogService implements IFileDialogService { } protected addFileToRecentlyOpened(uri: URI): void { + // add the picked file into the list of recently opened // only if it is outside the currently opened workspace + if (!this.contextService.isInsideWorkspace(uri)) { this.workspacesService.addRecentlyOpened([{ fileUri: uri, label: this.labelService.getUriLabel(uri) }]); } diff --git a/src/vs/workbench/services/dialogs/browser/fileDialogService.ts b/src/vs/workbench/services/dialogs/browser/fileDialogService.ts index cbb7a21bcba..1fc0ccc3570 100644 --- a/src/vs/workbench/services/dialogs/browser/fileDialogService.ts +++ b/src/vs/workbench/services/dialogs/browser/fileDialogService.ts @@ -223,29 +223,28 @@ export class FileDialogService extends AbstractFileDialogService implements IFil // Otherwise inform the user about options const buttons = context === 'open' ? - [localize('openRemote', "Open Remote..."), localize('openFiles', "Open Files..."), localize('learnMore', "Learn More")] : + [localize('openRemote', "Open Remote..."), localize('learnMore', "Learn More"), localize('openFiles', "Open Files...")] : [localize('openRemote', "Open Remote..."), localize('learnMore', "Learn More")]; const res = await this.dialogService.show( Severity.Warning, - localize('unsupportedBrowserMessage', "Local File System Access is Unsupported"), + localize('unsupportedBrowserMessage', "Opening Local Folders is Unsupported"), buttons, { - detail: localize('unsupportedBrowserDetail', "Your current browser doesn't support local file system access.\nYou can either open single files or open a remote repository."), + detail: localize('unsupportedBrowserDetail', "Your browser doesn't support opening local folders.\nYou can either open single files or open a remote repository."), cancelId: -1 // no "Cancel" button offered } ); switch (res.choice) { - - // Open Remote... case 0: this.commandService.executeCommand('workbench.action.remote.showMenu'); break; - - // Open Files... (context === 'open') case 1: - if (context === 'open') { + this.openerService.open('https://aka.ms/VSCodeWebLocalFileSystemAccess'); + break; + case 2: + { const files = await triggerUpload(); if (files) { const filesData = (await this.instantiationService.invokeFunction(accessor => extractFileListData(accessor, files))).filter(fileData => !fileData.isDirectory); @@ -259,14 +258,7 @@ export class FileDialogService extends AbstractFileDialogService implements IFil })); } } - break; - } else { - // Fallthrough for "Learn More" } - - // Learn More - case 2: - this.openerService.open('https://aka.ms/VSCodeWebLocalFileSystemAccess'); break; } diff --git a/src/vs/workbench/services/dialogs/common/dialogService.ts b/src/vs/workbench/services/dialogs/common/dialogService.ts index d17f9fbbf5c..7360fe7d875 100644 --- a/src/vs/workbench/services/dialogs/common/dialogService.ts +++ b/src/vs/workbench/services/dialogs/common/dialogService.ts @@ -8,6 +8,8 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { IConfirmation, IConfirmationResult, IDialogOptions, IDialogService, IInput, IInputResult, IShowResult } from 'vs/platform/dialogs/common/dialogs'; import { DialogsModel } from 'vs/workbench/common/dialogs'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { ILogService } from 'vs/platform/log/common/log'; export class DialogService extends Disposable implements IDialogService { @@ -19,25 +21,58 @@ export class DialogService extends Disposable implements IDialogService { readonly onDidShowDialog = this.model.onDidShowDialog; + constructor( + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @ILogService private readonly logService: ILogService + ) { + super(); + } + + private skipDialogs(): boolean { + if (this.environmentService.isExtensionDevelopment && this.environmentService.extensionTestsLocationURI) { + return true; // integration tests + } + + return !!this.environmentService.enableSmokeTestDriver; // smoke tests + } + async confirm(confirmation: IConfirmation): Promise<IConfirmationResult> { + if (this.skipDialogs()) { + this.logService.trace('DialogService: refused to show confirmation dialog in tests.'); + + return { confirmed: true }; + } + const handle = this.model.show({ confirmArgs: { confirmation } }); return await handle.result as IConfirmationResult; } async show(severity: Severity, message: string, buttons?: string[], options?: IDialogOptions): Promise<IShowResult> { + if (this.skipDialogs()) { + throw new Error('DialogService: refused to show dialog in tests.'); + } + const handle = this.model.show({ showArgs: { severity, message, buttons, options } }); return await handle.result as IShowResult; } async input(severity: Severity, message: string, buttons: string[], inputs: IInput[], options?: IDialogOptions): Promise<IInputResult> { + if (this.skipDialogs()) { + throw new Error('DialogService: refused to show input dialog in tests.'); + } + const handle = this.model.show({ inputArgs: { severity, message, buttons, inputs, options } }); return await handle.result as IInputResult; } async about(): Promise<void> { + if (this.skipDialogs()) { + throw new Error('DialogService: refused to show about dialog in tests.'); + } + const handle = this.model.show({}); await handle.result; } diff --git a/src/vs/workbench/services/dialogs/electron-sandbox/fileDialogService.ts b/src/vs/workbench/services/dialogs/electron-sandbox/fileDialogService.ts index e4468a73200..dbd0f777b07 100644 --- a/src/vs/workbench/services/dialogs/electron-sandbox/fileDialogService.ts +++ b/src/vs/workbench/services/dialogs/electron-sandbox/fileDialogService.ts @@ -25,6 +25,7 @@ import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { ILogService } from 'vs/platform/log/common/log'; export class FileDialogService extends AbstractFileDialogService implements IFileDialogService { @@ -45,17 +46,18 @@ export class FileDialogService extends AbstractFileDialogService implements IFil @IPathService pathService: IPathService, @ICommandService commandService: ICommandService, @IEditorService editorService: IEditorService, - @ICodeEditorService codeEditorService: ICodeEditorService + @ICodeEditorService codeEditorService: ICodeEditorService, + @ILogService logService: ILogService ) { super(hostService, contextService, historyService, environmentService, instantiationService, - configurationService, fileService, openerService, dialogService, languageService, workspacesService, labelService, pathService, commandService, editorService, codeEditorService); + configurationService, fileService, openerService, dialogService, languageService, workspacesService, labelService, pathService, commandService, editorService, codeEditorService, logService); } private toNativeOpenDialogOptions(options: IPickAndOpenOptions): INativeOpenDialogOptions { return { forceNewWindow: options.forceNewWindow, telemetryExtraData: options.telemetryExtraData, - defaultPath: options.defaultUri && options.defaultUri.fsPath + defaultPath: options.defaultUri?.fsPath }; } @@ -140,7 +142,7 @@ export class FileDialogService extends AbstractFileDialogService implements IFil private toNativeSaveDialogOptions(options: ISaveDialogOptions): SaveDialogOptions { options.defaultUri = options.defaultUri ? URI.file(options.defaultUri.path) : undefined; return { - defaultPath: options.defaultUri && options.defaultUri.fsPath, + defaultPath: options.defaultUri?.fsPath, buttonLabel: options.saveLabel, filters: options.filters, title: options.title @@ -167,11 +169,9 @@ export class FileDialogService extends AbstractFileDialogService implements IFil return this.showOpenDialogSimplified(schema, options); } - const defaultUri = options.defaultUri; - const newOptions: OpenDialogOptions & { properties: string[] } = { title: options.title, - defaultPath: defaultUri && defaultUri.fsPath, + defaultPath: options.defaultUri?.fsPath, buttonLabel: options.openLabel, filters: options.filters, properties: [] diff --git a/src/vs/workbench/services/dialogs/test/electron-sandbox/fileDialogService.test.ts b/src/vs/workbench/services/dialogs/test/electron-sandbox/fileDialogService.test.ts index fbe3a518f48..d8ab94017c1 100644 --- a/src/vs/workbench/services/dialogs/test/electron-sandbox/fileDialogService.test.ts +++ b/src/vs/workbench/services/dialogs/test/electron-sandbox/fileDialogService.test.ts @@ -33,6 +33,7 @@ import { ICommandService } from 'vs/platform/commands/common/commands'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ILogService } from 'vs/platform/log/common/log'; class TestFileDialogService extends FileDialogService { constructor( @@ -53,10 +54,11 @@ class TestFileDialogService extends FileDialogService { @IPathService pathService: IPathService, @ICommandService commandService: ICommandService, @IEditorService editorService: IEditorService, - @ICodeEditorService codeEditorService: ICodeEditorService + @ICodeEditorService codeEditorService: ICodeEditorService, + @ILogService logService: ILogService ) { super(hostService, contextService, historyService, environmentService, instantiationService, configurationService, fileService, - openerService, nativeHostService, dialogService, languageService, workspacesService, labelService, pathService, commandService, editorService, codeEditorService); + openerService, nativeHostService, dialogService, languageService, workspacesService, labelService, pathService, commandService, editorService, codeEditorService, logService); } protected override getSimpleFileDialog() { @@ -125,7 +127,7 @@ suite('FileDialogService', function () { instantiationService.stub(IPathService, new class { defaultUriScheme: string = 'vscode-virtual-test'; userHome = async () => URI.file('/user/home'); - }); + } as IPathService); const dialogService = instantiationService.createInstance(TestFileDialogService, new TestSimpleFileDialog()); instantiationService.set(IFileDialogService, dialogService); const workspaceService: IWorkspaceEditingService = instantiationService.createInstance(BrowserWorkspaceEditingService); @@ -157,7 +159,7 @@ suite('FileDialogService', function () { instantiationService.stub(IPathService, new class { defaultUriScheme: string = Schemas.vscodeRemote; userHome = async () => URI.file('/user/home'); - }); + } as IPathService); const dialogService = instantiationService.createInstance(TestFileDialogService, new TestSimpleFileDialog()); instantiationService.set(IFileDialogService, dialogService); const workspaceService: IWorkspaceEditingService = instantiationService.createInstance(BrowserWorkspaceEditingService); diff --git a/src/vs/workbench/services/editor/browser/editorResolverService.ts b/src/vs/workbench/services/editor/browser/editorResolverService.ts index 9edd90ea440..481d60b1b3f 100644 --- a/src/vs/workbench/services/editor/browser/editorResolverService.ts +++ b/src/vs/workbench/services/editor/browser/editorResolverService.ts @@ -746,7 +746,9 @@ export class EditorResolverService extends Disposable implements IEditorResolver private sendEditorResolutionTelemetry(chosenInput: EditorInput): void { type editorResolutionClassification = { - viewType: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; owner: 'lramos15'; comment: 'The id of the editor opened. Used to gain an undertsanding of what editors are most popular' }; + viewType: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'The id of the editor opened. Used to gain an undertsanding of what editors are most popular' }; + owner: 'lramos15'; + comment: 'An event that fires when an editor type is picked'; }; type editorResolutionEvent = { viewType: string; diff --git a/src/vs/workbench/services/editor/browser/editorService.ts b/src/vs/workbench/services/editor/browser/editorService.ts index 51015fde85a..4b5689f2e11 100644 --- a/src/vs/workbench/services/editor/browser/editorService.ts +++ b/src/vs/workbench/services/editor/browser/editorService.ts @@ -148,7 +148,7 @@ export class EditorService extends Disposable implements EditorServiceImpl { const groupDisposables = new DisposableStore(); groupDisposables.add(group.onDidModelChange(e => { - this._onDidEditorsChange.fire({ groupId: group.id, ...e }); + this._onDidEditorsChange.fire({ groupId: group.id, event: e }); })); groupDisposables.add(group.onDidActiveEditorChange(() => { diff --git a/src/vs/workbench/services/editor/common/editorGroupsService.ts b/src/vs/workbench/services/editor/common/editorGroupsService.ts index bdea82e86be..0802cbd5acf 100644 --- a/src/vs/workbench/services/editor/common/editorGroupsService.ts +++ b/src/vs/workbench/services/editor/common/editorGroupsService.ts @@ -652,7 +652,7 @@ export interface IEditorGroup { * * @returns a promise when all editors are closed. */ - closeAllEditors(options?: ICloseAllEditorsOptions): Promise<void>; + closeAllEditors(options?: ICloseAllEditorsOptions): Promise<boolean>; /** * Replaces editors in this group with the provided replacement. diff --git a/src/vs/workbench/services/editor/common/editorService.ts b/src/vs/workbench/services/editor/common/editorService.ts index 601636e9c48..32f2c303e1e 100644 --- a/src/vs/workbench/services/editor/common/editorService.ts +++ b/src/vs/workbench/services/editor/common/editorService.ts @@ -88,8 +88,15 @@ export interface IOpenEditorsOptions { readonly validateTrust?: boolean; } -export interface IEditorsChangeEvent extends IGroupModelChangeEvent { +export interface IEditorsChangeEvent { + /** + * The group which had the editor change + */ groupId: GroupIdentifier; + /* + * The event fired from the model + */ + event: IGroupModelChangeEvent; } export interface IEditorService { diff --git a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts index c52507b6659..83497325fa8 100644 --- a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts @@ -894,6 +894,7 @@ suite('EditorGroupsService', () => { test('closeAllEditors - dirty editor handling', async () => { const [part, instantiationService] = await createPart(); + let closeResult = true; const accessor = instantiationService.createInstance(TestServiceAccessor); accessor.fileDialogService.setConfirmResult(ConfirmResult.DONT_SAVE); @@ -909,14 +910,16 @@ suite('EditorGroupsService', () => { await group.openEditor(input2); accessor.fileDialogService.setConfirmResult(ConfirmResult.CANCEL); - await group.closeAllEditors(); + closeResult = await group.closeAllEditors(); + assert.strictEqual(closeResult, false); assert.ok(!input1.gotDisposed); assert.ok(!input2.gotDisposed); accessor.fileDialogService.setConfirmResult(ConfirmResult.DONT_SAVE); - await group.closeAllEditors(); + closeResult = await group.closeAllEditors(); + assert.strictEqual(closeResult, true); assert.ok(input1.gotDisposed); assert.ok(input2.gotDisposed); }); diff --git a/src/vs/workbench/services/editor/test/browser/editorService.test.ts b/src/vs/workbench/services/editor/test/browser/editorService.test.ts index 478a9c0fa50..12042e67927 100644 --- a/src/vs/workbench/services/editor/test/browser/editorService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorService.test.ts @@ -24,7 +24,7 @@ import { IWorkspaceTrustRequestService, WorkspaceTrustUriResponse } from 'vs/pla import { TestWorkspaceTrustRequestService } from 'vs/workbench/services/workspaces/test/common/testWorkspaceTrustService'; import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; -import { UnknownErrorEditor } from 'vs/workbench/browser/parts/editor/editorPlaceholder'; +import { ErrorPlaceholderEditor } from 'vs/workbench/browser/parts/editor/editorPlaceholder'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; @@ -2018,7 +2018,7 @@ suite('EditorService', () => { failingInput.setFailToOpen(); let failingEditor = await service.openEditor(failingInput); - assert.ok(failingEditor instanceof UnknownErrorEditor); + assert.ok(failingEditor instanceof ErrorPlaceholderEditor); }); test('openEditor shows placeholder when restoring fails', async function () { @@ -2032,7 +2032,7 @@ suite('EditorService', () => { failingInput.setFailToOpen(); let failingEditor = await service.openEditor(failingInput); - assert.ok(failingEditor instanceof UnknownErrorEditor); + assert.ok(failingEditor instanceof ErrorPlaceholderEditor); }); test('save, saveAll, revertAll', async function () { diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts index aec5f09e2ec..d41839b87ba 100644 --- a/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/src/vs/workbench/services/environment/browser/environmentService.ts @@ -17,6 +17,7 @@ import { parseLineAndColumnAware } from 'vs/base/common/extpath'; import { LogLevelToString } from 'vs/platform/log/common/log'; import { isUndefined } from 'vs/base/common/types'; import { refineServiceDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; export const IBrowserWorkbenchEnvironmentService = refineServiceDecorator<IEnvironmentService, IBrowserWorkbenchEnvironmentService>(IEnvironmentService); @@ -172,6 +173,9 @@ export class BrowserWorkbenchEnvironmentService implements IBrowserWorkbenchEnvi } @memoize + get enableSmokeTestDriver() { return this.options.developmentOptions?.enableSmokeTestDriver; } + + @memoize get disableExtensions() { return this.payload?.get('disableExtensions') === 'true'; } @memoize @@ -202,7 +206,7 @@ export class BrowserWorkbenchEnvironmentService implements IBrowserWorkbenchEnvi get logExtensionHostCommunication(): boolean { return this.payload?.get('logExtensionHostCommunication') === 'true'; } @memoize - get skipReleaseNotes(): boolean { return false; } + get skipReleaseNotes(): boolean { return this.payload?.get('skipReleaseNotes') === 'true'; } @memoize get skipWelcome(): boolean { return this.payload?.get('skipWelcome') === 'true'; } @@ -292,7 +296,7 @@ export class BrowserWorkbenchEnvironmentService implements IBrowserWorkbenchEnvi } @memoize - get filesToOpenOrCreate(): IPath[] | undefined { + get filesToOpenOrCreate(): IPath<ITextEditorOptions>[] | undefined { if (this.payload) { const fileToOpen = this.payload.get('openFile'); if (fileToOpen) { @@ -304,7 +308,9 @@ export class BrowserWorkbenchEnvironmentService implements IBrowserWorkbenchEnvi return [{ fileUri: fileUri.with({ path: pathColumnAware.path }), - selection: !isUndefined(pathColumnAware.line) ? { startLineNumber: pathColumnAware.line, startColumn: pathColumnAware.column || 1 } : undefined + options: { + selection: !isUndefined(pathColumnAware.line) ? { startLineNumber: pathColumnAware.line, startColumn: pathColumnAware.column || 1 } : undefined + } }]; } diff --git a/src/vs/workbench/services/environment/common/environmentService.ts b/src/vs/workbench/services/environment/common/environmentService.ts index 15e8bac2238..d9e2e3ba65e 100644 --- a/src/vs/workbench/services/environment/common/environmentService.ts +++ b/src/vs/workbench/services/environment/common/environmentService.ts @@ -39,6 +39,7 @@ export interface IWorkbenchEnvironmentService extends IEnvironmentService { // --- Development readonly debugRenderer: boolean; readonly logExtensionHostCommunication?: boolean; + readonly enableSmokeTestDriver?: boolean; // --- Editors to open readonly filesToOpenOrCreate?: IPath[] | undefined; diff --git a/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts b/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts index ab7626e36cb..f675b6800ed 100644 --- a/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts +++ b/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts @@ -103,6 +103,9 @@ export class NativeWorkbenchEnvironmentService extends AbstractNativeEnvironment get logExtensionHostCommunication(): boolean { return !!this.args.logExtensionHostCommunication; } @memoize + get enableSmokeTestDriver(): boolean { return !!this.args['enable-smoke-test-driver']; } + + @memoize get extensionEnabledProposedApi(): string[] | undefined { if (Array.isArray(this.args['enable-proposed-api'])) { return this.args['enable-proposed-api']; diff --git a/src/vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.ts b/src/vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.ts index c88dede88ba..089434c2a84 100644 --- a/src/vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.ts +++ b/src/vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.ts @@ -58,6 +58,8 @@ export class BuiltinExtensionsScannerService implements IBuiltinExtensionsScanne readmeUrl: e.readmePath ? uriIdentityService.extUri.joinPath(builtinExtensionsServiceUrl!, e.readmePath) : undefined, changelogUrl: e.changelogPath ? uriIdentityService.extUri.joinPath(builtinExtensionsServiceUrl!, e.changelogPath) : undefined, targetPlatform: TargetPlatform.WEB, + validations: [], + isValid: true })); } } diff --git a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts index f57e3cb5db6..1040b35beff 100644 --- a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts +++ b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts @@ -182,6 +182,11 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench } async setEnablement(extensions: IExtension[], newState: EnablementState): Promise<boolean[]> { + await this.extensionsManager.whenInitialized(); + + if (newState === EnablementState.EnabledGlobally || newState === EnablementState.EnabledWorkspace) { + extensions.push(...this.getExtensionsToEnableRecursively(extensions, this.extensionsManager.extensions, newState, { dependencies: true, pack: true })); + } const workspace = newState === EnablementState.DisabledWorkspace || newState === EnablementState.EnabledWorkspace; for (const extension of extensions) { @@ -213,6 +218,33 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench return result; } + private getExtensionsToEnableRecursively(extensions: IExtension[], allExtensions: ReadonlyArray<IExtension>, enablementState: EnablementState, options: { dependencies: boolean; pack: boolean }, checked: IExtension[] = []): IExtension[] { + const toCheck = extensions.filter(e => checked.indexOf(e) === -1); + if (toCheck.length) { + for (const extension of toCheck) { + checked.push(extension); + } + const extensionsToDisable = allExtensions.filter(i => { + if (checked.indexOf(i) !== -1) { + return false; + } + if (this.getEnablementState(i) === enablementState) { + return false; + } + return (options.dependencies || options.pack) + && extensions.some(extension => + (options.dependencies && extension.manifest.extensionDependencies?.some(id => areSameExtensions({ id }, i.identifier))) + || (options.pack && extension.manifest.extensionPack?.some(id => areSameExtensions({ id }, i.identifier))) + ); + }); + if (extensionsToDisable.length) { + extensionsToDisable.push(...this.getExtensionsToEnableRecursively(extensionsToDisable, allExtensions, enablementState, options, checked)); + } + return extensionsToDisable; + } + return []; + } + private _setUserEnablementState(extension: IExtension, newState: EnablementState): Promise<boolean> { const currentState = this._getUserEnablementState(extension.identifier); diff --git a/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts b/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts index 92ec5bd6f8d..8438e2f3377 100644 --- a/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts +++ b/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts @@ -36,8 +36,9 @@ import { IExtensionStorageService } from 'vs/platform/extensionManagement/common import { isNonEmptyArray } from 'vs/base/common/arrays'; import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { ExtensionManifestValidator } from 'vs/workbench/services/extensions/common/extensionPoints'; import { IProductService } from 'vs/platform/product/common/productService'; +import { validateExtensionManifest } from 'vs/platform/extensions/common/extensionValidator'; +import Severity from 'vs/base/common/severity'; type GalleryExtensionInfo = { readonly id: string; preRelease?: boolean; migrateStorageFrom?: string }; type ExtensionInfo = { readonly id: string; preRelease: boolean }; @@ -196,8 +197,6 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten const extension = await this.toScannedExtension(webExtension, true); if (extension.isValid || !scanOptions?.skipInvalidExtensions) { result.push(extension); - } else { - this.logService.info(`Ignoring additional builtin extension ${webExtension.identifier.id} because it is not valid.`, extension.validationMessages); } } catch (error) { this.logService.info(`Error while fetching the additional builtin extension ${location.toString()}.`, getErrorMessage(error)); @@ -225,8 +224,6 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten const extension = await this.toScannedExtension(webExtension, true); if (extension.isValid || !scanOptions?.skipInvalidExtensions) { result.push(extension); - } else { - this.logService.info(`Ignoring additional builtin extension ${webExtension.identifier.id} because it is not valid.`, extension.validationMessages); } } catch (error) { this.logService.info(`Ignoring additional builtin extension ${webExtension.identifier.id} because there is an error while converting it into scanned extension`, getErrorMessage(error)); @@ -493,19 +490,9 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten if (existing && semver.gt(existing.manifest.version, webExtension.version)) { continue; } - try { - const extension = await this.toScannedExtension(webExtension, false); - if (extension.isValid || !scanOptions?.skipInvalidExtensions) { - result.set(extension.identifier.id.toLowerCase(), extension); - } else { - this.logService.info(`Skipping user installed extension ${webExtension.identifier.id} because it is not valid.`, extension.validationMessages); - } - } catch (error) { - if (scanOptions?.bailOut) { - throw error; - } else { - this.logService.error(error, 'Error while scanning user extension', webExtension.identifier.id); - } + const extension = await this.toScannedExtension(webExtension, false); + if (extension.isValid || !scanOptions?.skipInvalidExtensions) { + result.set(extension.identifier.id.toLowerCase(), extension); } } return [...result.values()]; @@ -562,26 +549,50 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten private async toScannedExtension(webExtension: IWebExtension, isBuiltin: boolean, type: ExtensionType = ExtensionType.User): Promise<IScannedExtension> { const url = joinPath(webExtension.location, 'package.json'); - let content; + const validations: [Severity, string][] = []; + let content: string | undefined; try { content = await this.extensionResourceLoaderService.readExtensionResource(url); + if (!content) { + validations.push([Severity.Error, `Error while fetching package.json from the location '${url}'. Server returned no content`]); + } } catch (error) { - throw new Error(`Error while fetching package.json for extension '${webExtension.identifier.id}' from the location '${url}'. ${getErrorMessage(error)}`); + validations.push([Severity.Error, `Error while fetching package.json from the location '${url}'. ${getErrorMessage(error)}`]); } - if (!content) { - throw new Error(`Error while fetching package.json for extension '${webExtension.identifier.id}'. Server returned no content for the request '${url}'`); + let manifest: IExtensionManifest | null = null; + if (content) { + try { + manifest = JSON.parse(content); + } catch (error) { + validations.push([Severity.Error, `Error while parsing package.json. ${getErrorMessage(error)}`]); + } + } + + if (!manifest) { + const [publisher, name] = webExtension.identifier.id.split('.'); + manifest = { + name, + publisher, + version: webExtension.version, + engines: { vscode: '*' }, + }; } - let manifest: IExtensionManifest = JSON.parse(content); if (webExtension.packageNLSUri) { manifest = await this.translateManifest(manifest, webExtension.packageNLSUri); } const uuid = (<IGalleryMetadata | undefined>webExtension.metadata)?.id; - const validationMessages: string[] = []; - const isValid = ExtensionManifestValidator.isValidExtensionManifest(this.productService.version, this.productService.date, webExtension.location, manifest, false, validationMessages); + validations.push(...validateExtensionManifest(this.productService.version, this.productService.date, webExtension.location, manifest, false)); + let isValid = true; + for (const [severity, message] of validations) { + if (severity === Severity.Error) { + isValid = false; + this.logService.error(message); + } + } return { identifier: { id: webExtension.identifier.id, uuid: webExtension.identifier.uuid || uuid }, @@ -593,7 +604,7 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten changelogUrl: webExtension.changelogUri, metadata: webExtension.metadata, targetPlatform: TargetPlatform.WEB, - validationMessages, + validations, isValid }; } diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts index efb907e741c..c3ab212906b 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts @@ -140,11 +140,9 @@ export interface IWorkbenchExtensionEnablementService { export interface IScannedExtension extends IExtension { readonly metadata?: Metadata; - readonly isValid: boolean; - readonly validationMessages: readonly string[]; } -export type ScanOptions = { readonly bailOut?: boolean; readonly skipInvalidExtensions?: boolean }; +export type ScanOptions = { readonly skipInvalidExtensions?: boolean }; export const IWebExtensionsScannerService = createDecorator<IWebExtensionsScannerService>('IWebExtensionsScannerService'); export interface IWebExtensionsScannerService { diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index 1b34c6a0604..3c2475d48c0 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -21,7 +21,6 @@ import { IDownloadService } from 'vs/platform/download/common/download'; import { flatten } from 'vs/base/common/arrays'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import Severity from 'vs/base/common/severity'; -import { canceled } from 'vs/base/common/errors'; import { IUserDataSyncEnablementService, SyncResource } from 'vs/platform/userDataSync/common/userDataSync'; import { Promises } from 'vs/base/common/async'; import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; @@ -31,6 +30,7 @@ import { ICommandService } from 'vs/platform/commands/common/commands'; import { isUndefined } from 'vs/base/common/types'; import { IFileService } from 'vs/platform/files/common/files'; import { ILogService } from 'vs/platform/log/common/log'; +import { CancellationError } from 'vs/base/common/errors'; export class ExtensionManagementService extends Disposable implements IWorkbenchExtensionManagementService { @@ -74,8 +74,8 @@ export class ExtensionManagementService extends Disposable implements IWorkbench this.onDidUninstallExtension = this._register(this.servers.reduce((emitter: EventMultiplexer<DidUninstallExtensionOnServerEvent>, server) => { emitter.add(Event.map(server.extensionManagementService.onDidUninstallExtension, e => ({ ...e, server }))); return emitter; }, new EventMultiplexer<DidUninstallExtensionOnServerEvent>())).event; } - async getInstalled(type?: ExtensionType, donotIgnoreInvalidExtensions?: boolean): Promise<ILocalExtension[]> { - const result = await Promise.all(this.servers.map(({ extensionManagementService }) => extensionManagementService.getInstalled(type, donotIgnoreInvalidExtensions))); + async getInstalled(type?: ExtensionType): Promise<ILocalExtension[]> { + const result = await Promise.all(this.servers.map(({ extensionManagementService }) => extensionManagementService.getInstalled(type))); return flatten(result); } @@ -368,7 +368,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench case 1: return true; } - throw canceled(); + throw new CancellationError(); } return false; } @@ -402,7 +402,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench }); if (trustState === undefined) { - throw canceled(); + throw new CancellationError(); } } } @@ -462,7 +462,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench // Unfortunately ICommandService cannot be used directly due to cyclic dependencies this.instantiationService.invokeFunction(accessor => accessor.get(ICommandService).executeCommand('extension.open', extension.identifier.id, 'extensionPack')); } - throw canceled(); + throw new CancellationError(); } diff --git a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts index c1380b91d5f..28a5b281506 100644 --- a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts @@ -45,14 +45,14 @@ export class WebExtensionManagementService extends AbstractExtensionManagementSe return false; } - async getInstalled(type?: ExtensionType, bailOut?: boolean): Promise<ILocalExtension[]> { + async getInstalled(type?: ExtensionType): Promise<ILocalExtension[]> { const extensions = []; if (type === undefined || type === ExtensionType.System) { const systemExtensions = await this.webExtensionsScannerService.scanSystemExtensions(); extensions.push(...systemExtensions); } if (type === undefined || type === ExtensionType.User) { - const userExtensions = await this.webExtensionsScannerService.scanUserExtensions({ bailOut }); + const userExtensions = await this.webExtensionsScannerService.scanUserExtensions(); extensions.push(...userExtensions); } return Promise.all(extensions.map(e => toLocalExtension(e))); diff --git a/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts index 6fd1cff558a..9888caf5571 100644 --- a/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts @@ -107,6 +107,10 @@ export class NativeRemoteExtensionManagementService extends ExtensionManagementC throw new ExtensionManagementError(localize('notFoundCompatiblePrereleaseDependency', "Can't install pre-release version of '{0}' extension because it is not compatible with the current version of {1} (version {2}).", extension.identifier.id, this.productService.nameLong, this.productService.version), ExtensionManagementErrorCode.IncompatiblePreRelease); } } else { + /** If no compatible release version is found, check if the extension has a release version or not and throw relevant error */ + if (!includePreRelease && extension.properties.isPreReleaseVersion && (await this.galleryService.getExtensions([extension.identifier], CancellationToken.None))[0]) { + throw new ExtensionManagementError(localize('notFoundReleaseExtension', "Can't install release version of '{0}' extension because it has no release version.", extension.identifier.id), ExtensionManagementErrorCode.ReleaseVersionNotFound); + } throw new ExtensionManagementError(localize('notFoundCompatibleDependency', "Can't install '{0}' extension because it is not compatible with the current version of {1} (version {2}).", extension.identifier.id, this.productService.nameLong, this.productService.version), ExtensionManagementErrorCode.Incompatible); } diff --git a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts index 894fc37142d..660c2fcd2e7 100644 --- a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts +++ b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts @@ -386,6 +386,31 @@ suite('ExtensionEnablementService Test', () => { assert.strictEqual(testObject.getEnablementState(extension), EnablementState.EnabledGlobally); }); + test('test enable an extension also enables dependencies', async () => { + installed.push(...[aLocalExtension2('pub.a', { extensionDependencies: ['pub.b'] }), aLocalExtension('pub.b')]); + const target = installed[0]; + const dep = installed[1]; + await (<TestExtensionEnablementService>testObject).waitUntilInitialized(); + await testObject.setEnablement([dep, target], EnablementState.DisabledGlobally); + await testObject.setEnablement([target], EnablementState.EnabledGlobally); + assert.ok(testObject.isEnabled(target)); + assert.ok(testObject.isEnabled(dep)); + assert.strictEqual(testObject.getEnablementState(target), EnablementState.EnabledGlobally); + assert.strictEqual(testObject.getEnablementState(dep), EnablementState.EnabledGlobally); + }); + + test('test enable an extension also enables packed extensions', async () => { + installed.push(...[aLocalExtension2('pub.a', { extensionPack: ['pub.b'] }), aLocalExtension('pub.b')]); + const target = installed[0]; + const dep = installed[1]; + await testObject.setEnablement([dep, target], EnablementState.DisabledGlobally); + await testObject.setEnablement([target], EnablementState.EnabledGlobally); + assert.ok(testObject.isEnabled(target)); + assert.ok(testObject.isEnabled(dep)); + assert.strictEqual(testObject.getEnablementState(target), EnablementState.EnabledGlobally); + assert.strictEqual(testObject.getEnablementState(dep), EnablementState.EnabledGlobally); + }); + test('test remove an extension from disablement list when uninstalled', async () => { const extension = aLocalExtension('pub.a'); installed.push(extension); @@ -513,7 +538,7 @@ suite('ExtensionEnablementService Test', () => { const extension = aLocalExtension('pub.a'); installed.push(extension); - testObject.setEnablement([extension], EnablementState.EnabledWorkspace); + await testObject.setEnablement([extension], EnablementState.EnabledWorkspace); instantiationService.stub(IWorkbenchEnvironmentService, { enableExtensions: <readonly string[]>['pub.a'] } as IWorkbenchEnvironmentService); testObject = new TestExtensionEnablementService(instantiationService); @@ -525,7 +550,7 @@ suite('ExtensionEnablementService Test', () => { const extension = aLocalExtension('pub.a'); installed.push(extension); - testObject.setEnablement([extension], EnablementState.DisabledGlobally); + await testObject.setEnablement([extension], EnablementState.DisabledGlobally); instantiationService.stub(IWorkbenchEnvironmentService, { enableExtensions: <readonly string[]>['pub.a'] } as IWorkbenchEnvironmentService); testObject = new TestExtensionEnablementService(instantiationService); @@ -537,7 +562,7 @@ suite('ExtensionEnablementService Test', () => { const extension = aLocalExtension('pub.a'); installed.push(extension); - testObject.setEnablement([extension], EnablementState.DisabledWorkspace); + await testObject.setEnablement([extension], EnablementState.DisabledWorkspace); instantiationService.stub(IWorkbenchEnvironmentService, { enableExtensions: <readonly string[]>['pub.a'] } as IWorkbenchEnvironmentService); testObject = new TestExtensionEnablementService(instantiationService); diff --git a/src/vs/workbench/services/extensionResourceLoader/electron-sandbox/extensionResourceLoaderService.ts b/src/vs/workbench/services/extensionResourceLoader/electron-sandbox/extensionResourceLoaderService.ts index 471f36a8d0a..ee604f29b31 100644 --- a/src/vs/workbench/services/extensionResourceLoader/electron-sandbox/extensionResourceLoaderService.ts +++ b/src/vs/workbench/services/extensionResourceLoader/electron-sandbox/extensionResourceLoaderService.ts @@ -8,7 +8,7 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IFileService } from 'vs/platform/files/common/files'; import { AbstractExtensionResourceLoaderService, IExtensionResourceLoaderService } from 'vs/workbench/services/extensionResourceLoader/common/extensionResourceLoader'; import { IProductService } from 'vs/platform/product/common/productService'; -import { asText, IRequestService } from 'vs/platform/request/common/request'; +import { asTextOrError, IRequestService } from 'vs/platform/request/common/request'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -31,7 +31,7 @@ export class ExtensionResourceLoaderService extends AbstractExtensionResourceLoa if (this.isExtensionGalleryResource(uri)) { const headers = await this.getExtensionGalleryRequestHeaders(); const requestContext = await this._requestService.request({ url: uri.toString(), headers }, CancellationToken.None); - return (await asText(requestContext)) || ''; + return (await asTextOrError(requestContext)) || ''; } const result = await this._fileService.readFile(uri); return result.value.toString(); diff --git a/src/vs/workbench/services/extensions/browser/extensionService.ts b/src/vs/workbench/services/extensions/browser/extensionService.ts index 42a05f2344a..1384f29e5ac 100644 --- a/src/vs/workbench/services/extensions/browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/browser/extensionService.ts @@ -16,7 +16,7 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { AbstractExtensionService, ExtensionRunningPreference, extensionRunningPreferenceToString } from 'vs/workbench/services/extensions/common/abstractExtensionService'; import { RemoteExtensionHost, IRemoteExtensionHostDataProvider, IRemoteExtensionHostInitData } from 'vs/workbench/services/extensions/common/remoteExtensionHost'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; -import { IWebWorkerExtensionHostDataProvider, WebWorkerExtensionHost } from 'vs/workbench/services/extensions/browser/webWorkerExtensionHost'; +import { IWebWorkerExtensionHostDataProvider, IWebWorkerExtensionHostInitData, WebWorkerExtensionHost } from 'vs/workbench/services/extensions/browser/webWorkerExtensionHost'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ExtensionIdentifier, IExtensionDescription, IExtension, ExtensionType } from 'vs/platform/extensions/common/extensions'; import { ExtensionKind } from 'vs/platform/environment/common/environment'; @@ -108,12 +108,13 @@ export class ExtensionService extends AbstractExtensionService implements IExten private _createLocalExtensionHostDataProvider(desiredRunningLocation: ExtensionRunningLocation): IWebWorkerExtensionHostDataProvider { return { - getInitData: async () => { + getInitData: async (): Promise<IWebWorkerExtensionHostInitData> => { const allExtensions = await this.getExtensions(); const localWebWorkerExtensions = this._filterByRunningLocation(allExtensions, desiredRunningLocation); return { autoStart: true, - extensions: localWebWorkerExtensions + allExtensions: allExtensions, + myExtensions: localWebWorkerExtensions.map(extension => extension.identifier) }; } }; @@ -232,8 +233,8 @@ export class ExtensionService extends AbstractExtensionService implements IExten extensionHostLogsPath: remoteEnv.extensionHostLogsPath, globalStorageHome: remoteEnv.globalStorageHome, workspaceStorageHome: remoteEnv.workspaceStorageHome, - extensions: remoteExtensions, - allExtensions: this._registry.getAllExtensionDescriptions() + allExtensions: this._registry.getAllExtensionDescriptions(), + myExtensions: remoteExtensions.map(extension => extension.identifier), }; } diff --git a/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts b/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts index 85bff25d857..31b1091efce 100644 --- a/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts +++ b/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts @@ -12,11 +12,11 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { ILabelService } from 'vs/platform/label/common/label'; import { ILogService } from 'vs/platform/log/common/log'; -import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import * as platform from 'vs/base/common/platform'; import * as dom from 'vs/base/browser/dom'; import { URI } from 'vs/base/common/uri'; -import { IExtensionHost, ExtensionHostLogFileName, LocalWebWorkerRunningLocation } from 'vs/workbench/services/extensions/common/extensions'; +import { IExtensionHost, ExtensionHostLogFileName, LocalWebWorkerRunningLocation, ExtensionHostExtensions } from 'vs/workbench/services/extensions/common/extensions'; import { IProductService } from 'vs/platform/product/common/productService'; import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; import { joinPath } from 'vs/base/common/resources'; @@ -30,11 +30,11 @@ import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { FileAccess } from 'vs/base/common/network'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { parentOriginHash } from 'vs/workbench/browser/webview'; -import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; export interface IWebWorkerExtensionHostInitData { readonly autoStart: boolean; - readonly extensions: IExtensionDescription[]; + readonly allExtensions: IExtensionDescription[]; + readonly myExtensions: ExtensionIdentifier[]; } export interface IWebWorkerExtensionHostDataProvider { @@ -45,7 +45,7 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost public readonly remoteAuthority = null; public readonly lazyStart: boolean; - public readonly extensions = new ExtensionDescriptionRegistry([]); + public readonly extensions = new ExtensionHostExtensions(); private readonly _onDidExit = this._register(new Emitter<[number, string | null]>()); public readonly onExit: Event<[number, string | null]> = this._onDidExit.event; @@ -130,6 +130,7 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost const iframe = document.createElement('iframe'); iframe.setAttribute('class', 'web-worker-ext-host-iframe'); iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin'); + iframe.setAttribute('aria-hidden', 'true'); iframe.style.display = 'none'; const vscodeWebWorkerExtHostId = generateUuid(); @@ -267,7 +268,7 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost private async _createExtHostInitData(): Promise<IExtensionHostInitData> { const [telemetryInfo, initData] = await Promise.all([this._telemetryService.getTelemetryInfo(), this._initDataProvider.getInitData()]); const workspace = this._contextService.getWorkspace(); - this.extensions.deltaExtensions(initData.extensions, []); + const deltaExtensions = this.extensions.set(initData.allExtensions, initData.myExtensions); return { commit: this._productService.commit, version: this._productService.version, @@ -289,9 +290,8 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost name: this._labelService.getWorkspaceLabel(workspace), transient: workspace.transient }, - resolvedExtensions: [], - hostExtensions: [], - extensions: this.extensions.getAllExtensionDescriptions(), + allExtensions: deltaExtensions.toAdd, + myExtensions: deltaExtensions.myToAdd, telemetryInfo, logLevel: this._logService.getLevel(), logsLocation: this._extensionHostLogsLocation, diff --git a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts index 86784926919..fc7e02bdcfc 100644 --- a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts +++ b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts @@ -32,7 +32,6 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; -import { ILog } from 'vs/workbench/services/extensions/common/extensionPoints'; import { dedupExtensions } from 'vs/workbench/services/extensions/common/extensionsUtil'; import { ApiProposalName, allApiProposals } from 'vs/workbench/services/extensions/common/extensionsApiProposals'; import { forEach } from 'vs/base/common/collections'; @@ -629,12 +628,10 @@ export abstract class AbstractExtensionService extends Disposable implements IEx await Promise.all(promises); } - private async _updateExtensionsOnExtHost(extensionHostManager: IExtensionHostManager, _toAdd: IExtensionDescription[], _toRemove: ExtensionIdentifier[], removedRunningLocation: Map<string, ExtensionRunningLocation | null>): Promise<void> { - const toAdd = filterByExtensionHostManager(_toAdd, this._runningLocation, extensionHostManager); - const toRemove = _filterByExtensionHostManager(_toRemove, extId => extId, removedRunningLocation, extensionHostManager); - if (toRemove.length > 0 || toAdd.length > 0) { - await extensionHostManager.deltaExtensions(toAdd, toRemove); - } + private async _updateExtensionsOnExtHost(extensionHostManager: IExtensionHostManager, toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[], removedRunningLocation: Map<string, ExtensionRunningLocation | null>): Promise<void> { + const myToAdd = filterByExtensionHostManager(toAdd, this._runningLocation, extensionHostManager); + const myToRemove = _filterByExtensionHostManager(toRemove, extId => extId, removedRunningLocation, extensionHostManager); + await extensionHostManager.deltaExtensions({ toRemove, toAdd, myToRemove, myToAdd: myToAdd.map(extension => extension.identifier) }); } public canAddExtension(extension: IExtensionDescription): boolean { @@ -1245,26 +1242,6 @@ export abstract class AbstractExtensionService extends Disposable implements IEx //#region Called by extension host - protected _createLogger(): ILog { - return { - error: (message: string | Error): void => { - if (this._isDev) { - this._notificationService.notify({ severity: Severity.Error, message }); - } - this._logService.error(message); - }, - warn: (message: string): void => { - if (this._isDev) { - this._notificationService.notify({ severity: Severity.Warning, message }); - } - this._logService.warn(message); - }, - info: (message: string): void => { - this._logService.info(message); - } - }; - } - private _acquireInternalAPI(): IInternalExtensionService { return { _activateById: (extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise<void> => { @@ -1329,7 +1306,6 @@ export abstract class AbstractExtensionService extends Disposable implements IEx } protected async _scanWebExtensions(): Promise<IExtensionDescription[]> { - const log = this._createLogger(); const system: IExtensionDescription[] = [], user: IExtensionDescription[] = [], development: IExtensionDescription[] = []; try { await Promise.all([ @@ -1338,9 +1314,9 @@ export abstract class AbstractExtensionService extends Disposable implements IEx this._webExtensionsScannerService.scanExtensionsUnderDevelopment().then(extensions => development.push(...extensions.map(e => toExtensionDescription(e, true)))) ]); } catch (error) { - log.error(error); + this._logService.error(error); } - return dedupExtensions(system, user, development, log); + return dedupExtensions(system, user, development, this._logService); } //#endregion diff --git a/src/vs/workbench/services/extensions/common/extensionDescriptionRegistry.ts b/src/vs/workbench/services/extensions/common/extensionDescriptionRegistry.ts index b3cb9b17813..274bc9c396e 100644 --- a/src/vs/workbench/services/extensions/common/extensionDescriptionRegistry.ts +++ b/src/vs/workbench/services/extensions/common/extensionDescriptionRegistry.ts @@ -61,10 +61,8 @@ export class ExtensionDescriptionRegistry { } } - public keepOnly(extensionIds: ExtensionIdentifier[]): void { - const toKeep = new Set<string>(); - extensionIds.forEach(extensionId => toKeep.add(ExtensionIdentifier.toKey(extensionId))); - this._extensionDescriptions = this._extensionDescriptions.filter(extension => toKeep.has(ExtensionIdentifier.toKey(extension.identifier))); + public set(extensionDescriptions: IExtensionDescription[]) { + this._extensionDescriptions = extensionDescriptions; this._initialize(); this._onDidChange.fire(undefined); } diff --git a/src/vs/workbench/services/extensions/common/extensionHostManager.ts b/src/vs/workbench/services/extensions/common/extensionHostManager.ts index e51f3d09cc6..eb4c4437b6a 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostManager.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostManager.ts @@ -19,13 +19,14 @@ import { registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { StopWatch } from 'vs/base/common/stopwatch'; import { VSBuffer } from 'vs/base/common/buffer'; -import { IExtensionHost, ExtensionHostKind, ActivationKind, extensionHostKindToString, ExtensionActivationReason, IInternalExtensionService, ExtensionRunningLocation } from 'vs/workbench/services/extensions/common/extensions'; +import { IExtensionHost, ExtensionHostKind, ActivationKind, extensionHostKindToString, ExtensionActivationReason, IInternalExtensionService, ExtensionRunningLocation, ExtensionHostExtensions } from 'vs/workbench/services/extensions/common/extensions'; import { CATEGORIES } from 'vs/workbench/common/actions'; import { Barrier, timeout } from 'vs/base/common/async'; import { URI } from 'vs/base/common/uri'; import { ILogService } from 'vs/platform/log/common/log'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IExtensionHostProxy, IResolveAuthorityResult } from 'vs/workbench/services/extensions/common/extensionHostProxy'; +import { IExtensionDescriptionDelta } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; // Enable to see detailed message communication between window and extension host const LOG_EXTENSION_HOST_COMMUNICATION = false; @@ -39,7 +40,7 @@ export interface IExtensionHostManager { dispose(): void; ready(): Promise<void>; representsRunningLocation(runningLocation: ExtensionRunningLocation): boolean; - deltaExtensions(toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[]): Promise<void>; + deltaExtensions(extensionsDelta: IExtensionDescriptionDelta): Promise<void>; containsExtension(extensionId: ExtensionIdentifier): boolean; activate(extension: ExtensionIdentifier, reason: ExtensionActivationReason): Promise<boolean>; activateByEvent(activationEvent: string, activationKind: ActivationKind): Promise<void>; @@ -50,7 +51,7 @@ export interface IExtensionHostManager { * Returns `null` if no resolver for `remoteAuthority` is found. */ getCanonicalURI(remoteAuthority: string, uri: URI): Promise<URI | null>; - start(enabledExtensionIds: ExtensionIdentifier[]): Promise<void>; + start(allExtensions: IExtensionDescription[], myExtensions: ExtensionIdentifier[]): Promise<void>; extensionTestsExecute(): Promise<number>; extensionTestsSendExit(exitCode: number): Promise<void>; setRemoteEnvironment(env: { [key: string]: string | null }): Promise<void>; @@ -272,6 +273,7 @@ class ExtensionHostManager extends Disposable implements IExtensionHostManager { extensionHostKind: this.kind, getProxy: <T>(identifier: ProxyIdentifier<T>): Proxied<T> => this._rpcProtocol!.getProxy(identifier), set: <T, R extends T>(identifier: ProxyIdentifier<T>, instance: R): R => this._rpcProtocol!.set(identifier, instance), + dispose: (): void => this._rpcProtocol!.dispose(), assertRegistered: (identifiers: ProxyIdentifier<any>[]): void => this._rpcProtocol!.assertRegistered(identifiers), drain: (): Promise<void> => this._rpcProtocol!.drain(), @@ -397,13 +399,13 @@ class ExtensionHostManager extends Disposable implements IExtensionHostManager { return proxy.getCanonicalURI(remoteAuthority, uri); } - public async start(enabledExtensionIds: ExtensionIdentifier[]): Promise<void> { + public async start(allExtensions: IExtensionDescription[], myExtensions: ExtensionIdentifier[]): Promise<void> { const proxy = await this._proxy; if (!proxy) { return; } - this._extensionHost.extensions.keepOnly(enabledExtensionIds); - return proxy.startExtensionHost(enabledExtensionIds); + const deltaExtensions = this._extensionHost.extensions.set(allExtensions, myExtensions); + return proxy.startExtensionHost(deltaExtensions); } public async extensionTestsExecute(): Promise<number> { @@ -432,13 +434,13 @@ class ExtensionHostManager extends Disposable implements IExtensionHostManager { return this._extensionHost.runningLocation.equals(runningLocation); } - public async deltaExtensions(toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[]): Promise<void> { + public async deltaExtensions(extensionsDelta: IExtensionDescriptionDelta): Promise<void> { const proxy = await this._proxy; if (!proxy) { return; } - this._extensionHost.extensions.deltaExtensions(toAdd, toRemove); - return proxy.deltaExtensions(toAdd, toRemove); + this._extensionHost.extensions.delta(extensionsDelta); + return proxy.deltaExtensions(extensionsDelta); } public containsExtension(extensionId: ExtensionIdentifier): boolean { @@ -467,6 +469,7 @@ class LazyStartExtensionHostManager extends Disposable implements IExtensionHost private readonly _extensionHost: IExtensionHost; private _startCalled: Barrier; private _actual: ExtensionHostManager | null; + private _lazyStartExtensions: ExtensionHostExtensions | null; public get kind(): ExtensionHostKind { return this._extensionHost.runningLocation.kind; @@ -484,6 +487,7 @@ class LazyStartExtensionHostManager extends Disposable implements IExtensionHost this.onDidExit = extensionHost.onExit; this._startCalled = new Barrier(); this._actual = null; + this._lazyStartExtensions = null; } private _createActual(reason: string): ExtensionHostManager { @@ -499,7 +503,7 @@ class LazyStartExtensionHostManager extends Disposable implements IExtensionHost return this._actual; } const actual = this._createActual(reason); - await actual.start([]); + await actual.start([], []); return actual; } @@ -512,13 +516,17 @@ class LazyStartExtensionHostManager extends Disposable implements IExtensionHost public representsRunningLocation(runningLocation: ExtensionRunningLocation): boolean { return this._extensionHost.runningLocation.equals(runningLocation); } - public async deltaExtensions(toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[]): Promise<void> { + public async deltaExtensions(extensionsDelta: IExtensionDescriptionDelta): Promise<void> { await this._startCalled.wait(); - const extensionHostAlreadyStarted = Boolean(this._actual); - const shouldStartExtensionHost = (toAdd.length > 0); - if (extensionHostAlreadyStarted || shouldStartExtensionHost) { - const actual = await this._getOrCreateActualAndStart(`contains ${toAdd.length} new extension(s) (installed or enabled): ${toAdd.map(ext => ext.identifier.value)}`); - return actual.deltaExtensions(toAdd, toRemove); + if (this._actual) { + return this._actual.deltaExtensions(extensionsDelta); + } + this._lazyStartExtensions!.delta(extensionsDelta); + if (extensionsDelta.myToAdd.length > 0) { + const actual = this._createActual(`contains ${extensionsDelta.myToAdd.length} new extension(s) (installed or enabled): ${extensionsDelta.myToAdd.map(extId => extId.value)}`); + const { toAdd, myToAdd } = this._lazyStartExtensions!.toDelta(); + actual.start(toAdd, myToAdd); + return; } } public containsExtension(extensionId: ExtensionIdentifier): boolean { @@ -581,15 +589,17 @@ class LazyStartExtensionHostManager extends Disposable implements IExtensionHost } throw new Error(`Cannot resolve canonical URI`); } - public async start(enabledExtensionIds: ExtensionIdentifier[]): Promise<void> { - if (enabledExtensionIds.length > 0) { + public async start(allExtensions: IExtensionDescription[], myExtensions: ExtensionIdentifier[]): Promise<void> { + if (myExtensions.length > 0) { // there are actual extensions, so let's launch the extension host - const actual = this._createActual(`contains ${enabledExtensionIds.length} extension(s): ${enabledExtensionIds.map(extId => extId.value)}.`); - const result = actual.start(enabledExtensionIds); + const actual = this._createActual(`contains ${myExtensions.length} extension(s): ${myExtensions.map(extId => extId.value)}.`); + const result = actual.start(allExtensions, myExtensions); this._startCalled.open(); return result; } - // there are no actual extensions + // there are no actual extensions running, store extensions in `this._lazyStartExtensions` + this._lazyStartExtensions = new ExtensionHostExtensions(); + this._lazyStartExtensions.set(allExtensions, myExtensions); this._startCalled.open(); } public async extensionTestsExecute(): Promise<number> { diff --git a/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts b/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts index a090dced1df..ca72df6f92f 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts @@ -10,15 +10,21 @@ import { LogLevel } from 'vs/platform/log/common/log'; import { IRemoteConnectionData } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { ITelemetryInfo } from 'vs/platform/telemetry/common/telemetry'; +export interface IExtensionDescriptionDelta { + readonly toRemove: ExtensionIdentifier[]; + readonly toAdd: IExtensionDescription[]; + readonly myToRemove: ExtensionIdentifier[]; + readonly myToAdd: ExtensionIdentifier[]; +} + export interface IExtensionHostInitData { version: string; commit?: string; parentPid: number; environment: IEnvironment; workspace?: IStaticWorkspaceData | null; - resolvedExtensions: ExtensionIdentifier[]; - hostExtensions: ExtensionIdentifier[]; - extensions: IExtensionDescription[]; + allExtensions: IExtensionDescription[]; + myExtensions: ExtensionIdentifier[]; telemetryInfo: ITelemetryInfo; logLevel: LogLevel; logsLocation: URI; diff --git a/src/vs/workbench/services/extensions/common/extensionHostProxy.ts b/src/vs/workbench/services/extensions/common/extensionHostProxy.ts index d021dda0f0d..b528989a727 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostProxy.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostProxy.ts @@ -5,8 +5,9 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { URI } from 'vs/base/common/uri'; -import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { IRemoteConnectionData, RemoteAuthorityResolverErrorCode, ResolverResult } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { IExtensionDescriptionDelta } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { ActivationKind, ExtensionActivationReason } from 'vs/workbench/services/extensions/common/extensions'; export interface IResolveAuthorityErrorResult { @@ -31,14 +32,14 @@ export interface IExtensionHostProxy { * Returns `null` if no resolver for `remoteAuthority` is found. */ getCanonicalURI(remoteAuthority: string, uri: URI): Promise<URI | null>; - startExtensionHost(enabledExtensionIds: ExtensionIdentifier[]): Promise<void>; + startExtensionHost(extensionsDelta: IExtensionDescriptionDelta): Promise<void>; extensionTestsExecute(): Promise<number>; extensionTestsExit(code: number): Promise<void>; activateByEvent(activationEvent: string, activationKind: ActivationKind): Promise<void>; activate(extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise<boolean>; setRemoteEnvironment(env: { [key: string]: string | null }): Promise<void>; updateRemoteConnectionData(connectionData: IRemoteConnectionData): Promise<void>; - deltaExtensions(toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[]): Promise<void>; + deltaExtensions(extensionsDelta: IExtensionDescriptionDelta): Promise<void>; test_latency(n: number): Promise<number>; test_up(b: VSBuffer): Promise<number>; test_down(size: number): Promise<VSBuffer>; diff --git a/src/vs/workbench/services/extensions/common/extensionPoints.ts b/src/vs/workbench/services/extensions/common/extensionPoints.ts deleted file mode 100644 index 892afa14c54..00000000000 --- a/src/vs/workbench/services/extensions/common/extensionPoints.ts +++ /dev/null @@ -1,750 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as nls from 'vs/nls'; -import * as path from 'vs/base/common/path'; -import * as resources from 'vs/base/common/resources'; -import * as semver from 'vs/base/common/semver/semver'; -import * as json from 'vs/base/common/json'; -import * as arrays from 'vs/base/common/arrays'; -import { getParseErrorMessage } from 'vs/base/common/jsonErrorMessages'; -import * as types from 'vs/base/common/types'; -import { URI } from 'vs/base/common/uri'; -import { getGalleryExtensionId, getExtensionId, ExtensionKey } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { isValidExtensionVersion } from 'vs/platform/extensions/common/extensionValidator'; -import { ExtensionIdentifier, IExtensionDescription, IExtensionManifest, IRelaxedExtensionDescription, TargetPlatform, UNDEFINED_PUBLISHER } from 'vs/platform/extensions/common/extensions'; -import { Metadata } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { FileOperationResult, IFileService, toFileOperationResult } from 'vs/platform/files/common/files'; - -const MANIFEST_FILE = 'package.json'; - -export interface Translations { - [id: string]: string; -} - -export namespace Translations { - export function equals(a: Translations, b: Translations): boolean { - if (a === b) { - return true; - } - let aKeys = Object.keys(a); - let bKeys: Set<string> = new Set<string>(); - for (let key of Object.keys(b)) { - bKeys.add(key); - } - if (aKeys.length !== bKeys.size) { - return false; - } - - for (let key of aKeys) { - if (a[key] !== b[key]) { - return false; - } - bKeys.delete(key); - } - return bKeys.size === 0; - } -} - -export interface ILog { - error(message: string | Error): void; - warn(message: string): void; - info(message: string): void; -} - -export interface NlsConfiguration { - readonly devMode: boolean; - readonly locale: string | undefined; - readonly pseudo: boolean; - readonly translations: Translations; -} - -abstract class ExtensionManifestHandler { - - protected readonly _absoluteManifestPath: string; - - constructor( - protected readonly _ourVersion: string, - protected readonly _ourProductDate: string | undefined, - protected readonly _absoluteFolderPath: string, - protected readonly _isBuiltin: boolean, - protected readonly _isUnderDevelopment: boolean, - protected readonly _log: ILog, - protected readonly _fileService: IFileService - ) { - this._absoluteManifestPath = path.join(this._absoluteFolderPath, MANIFEST_FILE); - } - - protected _error(source: string, message: string): void { - this._log.error(`[${source}]: ${message}`); - } - - protected _warn(source: string, message: string): void { - this._log.warn(`[${source}]: ${message}`); - } - -} - -class ExtensionManifestParser extends ExtensionManifestHandler { - - private static _fastParseJSON<T>(text: string, errors: json.ParseError[]): T { - try { - return JSON.parse(text); - } catch (err) { - // invalid JSON, let's get good errors - return json.parse(text, errors); - } - } - - public parse(): Promise<IExtensionDescription | null> { - return readFile(this._fileService, this._absoluteManifestPath).then((manifestContents) => { - const errors: json.ParseError[] = []; - const manifest = ExtensionManifestParser._fastParseJSON<IRelaxedExtensionDescription & { __metadata?: Metadata }>(manifestContents, errors); - if (json.getNodeType(manifest) !== 'object') { - this._error(this._absoluteFolderPath, nls.localize('jsonParseInvalidType', "Invalid manifest file {0}: Not an JSON object.", this._absoluteManifestPath)); - } else if (errors.length === 0) { - manifest.uuid = manifest.__metadata?.id; - manifest.targetPlatform = manifest.__metadata?.targetPlatform ?? TargetPlatform.UNDEFINED; - manifest.isUserBuiltin = !!manifest.__metadata?.isBuiltin; - delete manifest.__metadata; - return manifest; - } else { - errors.forEach(e => { - this._error(this._absoluteFolderPath, nls.localize('jsonParseFail', "Failed to parse {0}: [{1}, {2}] {3}.", this._absoluteManifestPath, e.offset, e.length, getParseErrorMessage(e.error))); - }); - } - return null; - }, (err) => { - if (err.code === 'ENOENT') { - return null; - } - - this._error(this._absoluteFolderPath, nls.localize('fileReadFail', "Cannot read file {0}: {1}.", this._absoluteManifestPath, err.message)); - return null; - }); - } -} - -interface MessageBag { - [key: string]: string | { message: string; comment: string[] }; -} - -interface TranslationBundle { - contents: { - package: MessageBag; - }; -} - -interface LocalizedMessages { - values: MessageBag | undefined; - default: string | null; -} - -class ExtensionManifestNLSReplacer extends ExtensionManifestHandler { - - private readonly _nlsConfig: NlsConfiguration; - - constructor( - ourVersion: string, - ourProductDate: string | undefined, - absoluteFolderPath: string, - isBuiltin: boolean, - isUnderDevelopment: boolean, - nlsConfig: NlsConfiguration, - log: ILog, - fileService: IFileService - ) { - super(ourVersion, ourProductDate, absoluteFolderPath, isBuiltin, isUnderDevelopment, log, fileService); - this._nlsConfig = nlsConfig; - } - - public replaceNLS(extensionDescription: IExtensionDescription): Promise<IExtensionDescription> { - const reportErrors = (localized: string | null, errors: json.ParseError[]): void => { - errors.forEach((error) => { - this._error(this._absoluteFolderPath, nls.localize('jsonsParseReportErrors', "Failed to parse {0}: {1}.", localized, getParseErrorMessage(error.error))); - }); - }; - const reportInvalidFormat = (localized: string | null): void => { - this._error(this._absoluteFolderPath, nls.localize('jsonInvalidFormat', "Invalid format {0}: JSON object expected.", localized)); - }; - - let extension = path.extname(this._absoluteManifestPath); - let basename = this._absoluteManifestPath.substr(0, this._absoluteManifestPath.length - extension.length); - - const translationId = `${extensionDescription.publisher}.${extensionDescription.name}`; - let translationPath = this._nlsConfig.translations[translationId]; - let localizedMessages: Promise<LocalizedMessages | undefined>; - if (translationPath) { - localizedMessages = readFile(this._fileService, translationPath).then<LocalizedMessages, LocalizedMessages>((content) => { - let errors: json.ParseError[] = []; - let translationBundle: TranslationBundle = json.parse(content, errors); - if (errors.length > 0) { - reportErrors(translationPath, errors); - return { values: undefined, default: `${basename}.nls.json` }; - } else if (json.getNodeType(translationBundle) !== 'object') { - reportInvalidFormat(translationPath); - return { values: undefined, default: `${basename}.nls.json` }; - } else { - let values = translationBundle.contents ? translationBundle.contents.package : undefined; - return { values: values, default: `${basename}.nls.json` }; - } - }, (error) => { - return { values: undefined, default: `${basename}.nls.json` }; - }); - } else { - localizedMessages = existsFile(this._fileService, basename + '.nls' + extension).then<LocalizedMessages | undefined, LocalizedMessages | undefined>(exists => { - if (!exists) { - return undefined; - } - return ExtensionManifestNLSReplacer.findMessageBundles(this._nlsConfig, basename, this._fileService).then((messageBundle) => { - if (!messageBundle.localized) { - return { values: undefined, default: messageBundle.original }; - } - return readFile(this._fileService, messageBundle.localized).then(messageBundleContent => { - let errors: json.ParseError[] = []; - let messages: MessageBag = json.parse(messageBundleContent, errors); - if (errors.length > 0) { - reportErrors(messageBundle.localized, errors); - return { values: undefined, default: messageBundle.original }; - } else if (json.getNodeType(messages) !== 'object') { - reportInvalidFormat(messageBundle.localized); - return { values: undefined, default: messageBundle.original }; - } - return { values: messages, default: messageBundle.original }; - }, (err) => { - return { values: undefined, default: messageBundle.original }; - }); - }, (err) => { - return undefined; - }); - }); - } - - return localizedMessages.then((localizedMessages) => { - if (localizedMessages === undefined) { - return extensionDescription; - } - let errors: json.ParseError[] = []; - // resolveOriginalMessageBundle returns null if localizedMessages.default === undefined; - return this.resolveOriginalMessageBundle(localizedMessages.default, errors).then((defaults) => { - if (errors.length > 0) { - reportErrors(localizedMessages.default, errors); - return extensionDescription; - } else if (json.getNodeType(localizedMessages) !== 'object') { - reportInvalidFormat(localizedMessages.default); - return extensionDescription; - } - const localized = localizedMessages.values || Object.create(null); - ExtensionManifestNLSReplacer._replaceNLStrings(this._nlsConfig, extensionDescription, localized, defaults, this._absoluteFolderPath, this._log); - return extensionDescription; - }); - }, (err) => { - return extensionDescription; - }); - } - - /** - * Parses original message bundle, returns null if the original message bundle is null. - */ - private resolveOriginalMessageBundle(originalMessageBundle: string | null, errors: json.ParseError[]) { - return new Promise<{ [key: string]: string } | null>((c, e) => { - if (originalMessageBundle) { - readFile(this._fileService, originalMessageBundle).then(originalBundleContent => { - c(json.parse(originalBundleContent, errors)); - }, (err) => { - c(null); - }); - } else { - c(null); - } - }); - } - - /** - * Finds localized message bundle and the original (unlocalized) one. - * If the localized file is not present, returns null for the original and marks original as localized. - */ - private static findMessageBundles(nlsConfig: NlsConfiguration, basename: string, fileService: IFileService): Promise<{ localized: string; original: string | null }> { - return new Promise<{ localized: string; original: string | null }>((c, e) => { - function loop(basename: string, locale: string): void { - let toCheck = `${basename}.nls.${locale}.json`; - existsFile(fileService, toCheck).then(exists => { - if (exists) { - c({ localized: toCheck, original: `${basename}.nls.json` }); - } - let index = locale.lastIndexOf('-'); - if (index === -1) { - c({ localized: `${basename}.nls.json`, original: null }); - } else { - locale = locale.substring(0, index); - loop(basename, locale); - } - }); - } - - if (nlsConfig.devMode || nlsConfig.pseudo || !nlsConfig.locale) { - return c({ localized: basename + '.nls.json', original: null }); - } - loop(basename, nlsConfig.locale); - }); - } - - /** - * This routine makes the following assumptions: - * The root element is an object literal - */ - private static _replaceNLStrings<T extends object>(nlsConfig: NlsConfiguration, literal: T, messages: MessageBag, originalMessages: MessageBag | null, messageScope: string, log: ILog): void { - function processEntry(obj: any, key: string | number, command?: boolean) { - const value = obj[key]; - if (types.isString(value)) { - const str = <string>value; - const length = str.length; - if (length > 1 && str[0] === '%' && str[length - 1] === '%') { - const messageKey = str.substr(1, length - 2); - let translated = messages[messageKey]; - // If the messages come from a language pack they might miss some keys - // Fill them from the original messages. - if (translated === undefined && originalMessages) { - translated = originalMessages[messageKey]; - } - let message: string | undefined = typeof translated === 'string' ? translated : (typeof translated?.message === 'string' ? translated.message : undefined); - if (message !== undefined) { - if (nlsConfig.pseudo) { - // FF3B and FF3D is the Unicode zenkaku representation for [ and ] - message = '\uFF3B' + message.replace(/[aouei]/g, '$&$&') + '\uFF3D'; - } - obj[key] = command && (key === 'title' || key === 'category') && originalMessages ? { value: message, original: originalMessages[messageKey] } : message; - } else { - const message = nls.localize('missingNLSKey', "Couldn't find message for key {0}.", messageKey); - log.warn(`[${messageScope}]: ${message}`); - } - } - } else if (types.isObject(value)) { - for (let k in value) { - if (value.hasOwnProperty(k)) { - k === 'commands' ? processEntry(value, k, true) : processEntry(value, k, command); - } - } - } else if (types.isArray(value)) { - for (let i = 0; i < value.length; i++) { - processEntry(value, i, command); - } - } - } - - for (let key in literal) { - if (literal.hasOwnProperty(key)) { - processEntry(literal, key); - } - } - } -} - -export class ExtensionManifestValidator extends ExtensionManifestHandler { - validate(_extensionDescription: IExtensionDescription): IExtensionDescription | null { - let extensionDescription = <IRelaxedExtensionDescription>_extensionDescription; - extensionDescription.isBuiltin = this._isBuiltin; - extensionDescription.isUserBuiltin = !this._isBuiltin && !!extensionDescription.isUserBuiltin; - extensionDescription.isUnderDevelopment = this._isUnderDevelopment; - - let notices: string[] = []; - if (!ExtensionManifestValidator.isValidExtensionManifest(this._ourVersion, this._ourProductDate, URI.file(this._absoluteFolderPath), extensionDescription, extensionDescription.isBuiltin, notices)) { - notices.forEach((error) => { - this._error(this._absoluteFolderPath, error); - }); - return null; - } - - // in this case the notices are warnings - notices.forEach((error) => { - this._warn(this._absoluteFolderPath, error); - }); - - // allow publisher to be undefined to make the initial extension authoring experience smoother - if (!extensionDescription.publisher) { - extensionDescription.publisher = UNDEFINED_PUBLISHER; - } - - // id := `publisher.name` - extensionDescription.id = getExtensionId(extensionDescription.publisher, extensionDescription.name); - extensionDescription.identifier = new ExtensionIdentifier(extensionDescription.id); - - extensionDescription.extensionLocation = URI.file(this._absoluteFolderPath); - - return extensionDescription; - } - - public static isValidExtensionManifest(productVersion: string, productDate: string | undefined, extensionLocation: URI, extensionManifest: IExtensionManifest, extensionIsBuiltin: boolean, notices: string[]): boolean { - - if (!ExtensionManifestValidator.baseIsValidExtensionManifest(extensionLocation, extensionManifest, notices)) { - return false; - } - - if (!semver.valid(extensionManifest.version)) { - notices.push(nls.localize('notSemver', "Extension version is not semver compatible.")); - return false; - } - - return isValidExtensionVersion(productVersion, productDate, extensionManifest, extensionIsBuiltin, notices); - } - - private static baseIsValidExtensionManifest(extensionLocation: URI, extensionDescription: IExtensionManifest, notices: string[]): boolean { - if (!extensionDescription) { - notices.push(nls.localize('extensionDescription.empty', "Got empty extension description")); - return false; - } - if (typeof extensionDescription.publisher !== 'undefined' && typeof extensionDescription.publisher !== 'string') { - notices.push(nls.localize('extensionDescription.publisher', "property publisher must be of type `string`.")); - return false; - } - if (typeof extensionDescription.name !== 'string') { - notices.push(nls.localize('extensionDescription.name', "property `{0}` is mandatory and must be of type `string`", 'name')); - return false; - } - if (typeof extensionDescription.version !== 'string') { - notices.push(nls.localize('extensionDescription.version', "property `{0}` is mandatory and must be of type `string`", 'version')); - return false; - } - if (!extensionDescription.engines) { - notices.push(nls.localize('extensionDescription.engines', "property `{0}` is mandatory and must be of type `object`", 'engines')); - return false; - } - if (typeof extensionDescription.engines.vscode !== 'string') { - notices.push(nls.localize('extensionDescription.engines.vscode', "property `{0}` is mandatory and must be of type `string`", 'engines.vscode')); - return false; - } - if (typeof extensionDescription.extensionDependencies !== 'undefined') { - if (!ExtensionManifestValidator._isStringArray(extensionDescription.extensionDependencies)) { - notices.push(nls.localize('extensionDescription.extensionDependencies', "property `{0}` can be omitted or must be of type `string[]`", 'extensionDependencies')); - return false; - } - } - if (typeof extensionDescription.activationEvents !== 'undefined') { - if (!ExtensionManifestValidator._isStringArray(extensionDescription.activationEvents)) { - notices.push(nls.localize('extensionDescription.activationEvents1', "property `{0}` can be omitted or must be of type `string[]`", 'activationEvents')); - return false; - } - if (typeof extensionDescription.main === 'undefined' && typeof extensionDescription.browser === 'undefined') { - notices.push(nls.localize('extensionDescription.activationEvents2', "properties `{0}` and `{1}` must both be specified or must both be omitted", 'activationEvents', 'main')); - return false; - } - } - if (typeof extensionDescription.extensionKind !== 'undefined') { - if (typeof extensionDescription.main === 'undefined') { - notices.push(nls.localize('extensionDescription.extensionKind', "property `{0}` can be defined only if property `main` is also defined.", 'extensionKind')); - // not a failure case - } - } - if (typeof extensionDescription.main !== 'undefined') { - if (typeof extensionDescription.main !== 'string') { - notices.push(nls.localize('extensionDescription.main1', "property `{0}` can be omitted or must be of type `string`", 'main')); - return false; - } else { - const mainLocation = resources.joinPath(extensionLocation, extensionDescription.main); - if (!resources.isEqualOrParent(mainLocation, extensionLocation)) { - notices.push(nls.localize('extensionDescription.main2', "Expected `main` ({0}) to be included inside extension's folder ({1}). This might make the extension non-portable.", mainLocation.path, extensionLocation.path)); - // not a failure case - } - } - if (typeof extensionDescription.activationEvents === 'undefined') { - notices.push(nls.localize('extensionDescription.main3', "properties `{0}` and `{1}` must both be specified or must both be omitted", 'activationEvents', 'main')); - return false; - } - } - if (typeof extensionDescription.browser !== 'undefined') { - if (typeof extensionDescription.browser !== 'string') { - notices.push(nls.localize('extensionDescription.browser1', "property `{0}` can be omitted or must be of type `string`", 'browser')); - return false; - } else { - const browserLocation = resources.joinPath(extensionLocation, extensionDescription.browser); - if (!resources.isEqualOrParent(browserLocation, extensionLocation)) { - notices.push(nls.localize('extensionDescription.browser2', "Expected `browser` ({0}) to be included inside extension's folder ({1}). This might make the extension non-portable.", browserLocation.path, extensionLocation.path)); - // not a failure case - } - } - if (typeof extensionDescription.activationEvents === 'undefined') { - notices.push(nls.localize('extensionDescription.browser3', "properties `{0}` and `{1}` must both be specified or must both be omitted", 'activationEvents', 'browser')); - return false; - } - } - return true; - } - - private static _isStringArray(arr: string[]): boolean { - if (!Array.isArray(arr)) { - return false; - } - for (let i = 0, len = arr.length; i < len; i++) { - if (typeof arr[i] !== 'string') { - return false; - } - } - return true; - } -} - -export class ExtensionScannerInput { - - public mtime: number | undefined; - - constructor( - public readonly ourVersion: string, - public readonly ourProductDate: string | undefined, - public readonly commit: string | undefined, - public readonly locale: string | undefined, - public readonly devMode: boolean, - public readonly absoluteFolderPath: string, - public readonly isBuiltin: boolean, - public readonly isUnderDevelopment: boolean, - public readonly targetPlatform: TargetPlatform, - public readonly translations: Translations - ) { - // Keep empty!! (JSON.parse) - } - - public static createNLSConfig(input: ExtensionScannerInput): NlsConfiguration { - return { - devMode: input.devMode, - locale: input.locale, - pseudo: input.locale === 'pseudo', - translations: input.translations - }; - } - - public static equals(a: ExtensionScannerInput, b: ExtensionScannerInput): boolean { - return ( - a.ourVersion === b.ourVersion - && a.ourProductDate === b.ourProductDate - && a.commit === b.commit - && a.locale === b.locale - && a.devMode === b.devMode - && a.absoluteFolderPath === b.absoluteFolderPath - && a.isBuiltin === b.isBuiltin - && a.isUnderDevelopment === b.isUnderDevelopment - && a.mtime === b.mtime - && a.targetPlatform === b.targetPlatform - && Translations.equals(a.translations, b.translations) - ); - } -} - -export interface IExtensionReference { - name: string; - path: string; -} - -export interface IExtensionResolver { - resolveExtensions(): Promise<IExtensionReference[]>; -} - -class DefaultExtensionResolver implements IExtensionResolver { - - constructor( - private readonly root: string, - private readonly _fileService: IFileService - ) { - } - - resolveExtensions(): Promise<IExtensionReference[]> { - return readDirsInDir(this._fileService, this.root) - .then(folders => folders.map(name => ({ name, path: path.join(this.root, name) }))); - } -} - -export class ExtensionScanner { - - /** - * Read the extension defined in `absoluteFolderPath` - */ - private static scanExtension(version: string, productDate: string | undefined, absoluteFolderPath: string, isBuiltin: boolean, isUnderDevelopment: boolean, nlsConfig: NlsConfiguration, log: ILog, fileService: IFileService): Promise<IExtensionDescription | null> { - absoluteFolderPath = path.normalize(absoluteFolderPath); - - let parser = new ExtensionManifestParser(version, productDate, absoluteFolderPath, isBuiltin, isUnderDevelopment, log, fileService); - return parser.parse().then<IExtensionDescription | null>((extensionDescription) => { - if (extensionDescription === null) { - return null; - } - - let nlsReplacer = new ExtensionManifestNLSReplacer(version, productDate, absoluteFolderPath, isBuiltin, isUnderDevelopment, nlsConfig, log, fileService); - return nlsReplacer.replaceNLS(extensionDescription); - }).then((extensionDescription) => { - if (extensionDescription === null) { - return null; - } - - let validator = new ExtensionManifestValidator(version, productDate, absoluteFolderPath, isBuiltin, isUnderDevelopment, log, fileService); - return validator.validate(extensionDescription); - }); - } - - /** - * Scan a list of extensions defined in `absoluteFolderPath` - */ - public static async scanExtensions(input: ExtensionScannerInput, log: ILog, fileService: IFileService, resolver: IExtensionResolver | null = null): Promise<IExtensionDescription[]> { - const absoluteFolderPath = input.absoluteFolderPath; - const isBuiltin = input.isBuiltin; - const isUnderDevelopment = input.isUnderDevelopment; - - if (!resolver) { - resolver = new DefaultExtensionResolver(absoluteFolderPath, fileService); - } - - try { - let obsolete: { [folderName: string]: boolean } = {}; - if (!isBuiltin) { - try { - const obsoleteFileContents = await readFile(fileService, path.join(absoluteFolderPath, '.obsolete')); - obsolete = JSON.parse(obsoleteFileContents); - } catch (err) { - // Don't care - } - } - - let refs = await resolver.resolveExtensions(); - - // Ensure the same extension order - refs.sort((a, b) => a.name < b.name ? -1 : 1); - - if (!isBuiltin) { - refs = refs.filter(ref => ref.name.indexOf('.') !== 0); // Do not consider user extension folder starting with `.` - } - - const nlsConfig = ExtensionScannerInput.createNLSConfig(input); - let _extensionDescriptions = await Promise.all(refs.map(r => this.scanExtension(input.ourVersion, input.ourProductDate, r.path, isBuiltin, isUnderDevelopment, nlsConfig, log, fileService))); - let extensionDescriptions = arrays.coalesce(_extensionDescriptions); - extensionDescriptions = extensionDescriptions.filter(item => item !== null && !obsolete[new ExtensionKey({ id: getGalleryExtensionId(item.publisher, item.name) }, item.version, item.targetPlatform).toString()]); - - if (!isBuiltin) { - extensionDescriptions = this.filterOutdatedExtensions(extensionDescriptions, input.targetPlatform); - } - - extensionDescriptions.sort((a, b) => { - if (a.extensionLocation.fsPath < b.extensionLocation.fsPath) { - return -1; - } - return 1; - }); - return extensionDescriptions; - } catch (err) { - log.error(`Error scanning extensions at ${absoluteFolderPath}:`); - log.error(err); - return []; - } - } - - /** - * Combination of scanExtension and scanExtensions: If an extension manifest is found at root, we load just this extension, - * otherwise we assume the folder contains multiple extensions. - */ - public static scanOneOrMultipleExtensions(input: ExtensionScannerInput, log: ILog, fileService: IFileService): Promise<IExtensionDescription[]> { - const absoluteFolderPath = input.absoluteFolderPath; - const isBuiltin = input.isBuiltin; - const isUnderDevelopment = input.isUnderDevelopment; - - return existsFile(fileService, path.join(absoluteFolderPath, MANIFEST_FILE)).then((exists) => { - if (exists) { - const nlsConfig = ExtensionScannerInput.createNLSConfig(input); - return this.scanExtension(input.ourVersion, input.ourProductDate, absoluteFolderPath, isBuiltin, isUnderDevelopment, nlsConfig, log, fileService).then((extensionDescription) => { - if (extensionDescription === null) { - return []; - } - return [extensionDescription]; - }); - } - return this.scanExtensions(input, log, fileService); - }, (err) => { - log.error(`Error scanning extensions at ${absoluteFolderPath}:`); - log.error(err); - return []; - }); - } - - public static scanSingleExtension(input: ExtensionScannerInput, log: ILog, fileService: IFileService): Promise<IExtensionDescription | null> { - const absoluteFolderPath = input.absoluteFolderPath; - const isBuiltin = input.isBuiltin; - const isUnderDevelopment = input.isUnderDevelopment; - const nlsConfig = ExtensionScannerInput.createNLSConfig(input); - return this.scanExtension(input.ourVersion, input.ourProductDate, absoluteFolderPath, isBuiltin, isUnderDevelopment, nlsConfig, log, fileService); - } - - public static mergeBuiltinExtensions(builtinExtensions: Promise<IExtensionDescription[]>, extraBuiltinExtensions: Promise<IExtensionDescription[]>): Promise<IExtensionDescription[]> { - return Promise.all([builtinExtensions, extraBuiltinExtensions]).then(([builtinExtensions, extraBuiltinExtensions]) => { - let resultMap: { [id: string]: IExtensionDescription } = Object.create(null); - for (let i = 0, len = builtinExtensions.length; i < len; i++) { - resultMap[ExtensionIdentifier.toKey(builtinExtensions[i].identifier)] = builtinExtensions[i]; - } - // Overwrite with extensions found in extra - for (let i = 0, len = extraBuiltinExtensions.length; i < len; i++) { - resultMap[ExtensionIdentifier.toKey(extraBuiltinExtensions[i].identifier)] = extraBuiltinExtensions[i]; - } - - let resultArr = Object.keys(resultMap).map((id) => resultMap[id]); - resultArr.sort((a, b) => { - const aLastSegment = path.basename(a.extensionLocation.fsPath); - const bLastSegment = path.basename(b.extensionLocation.fsPath); - if (aLastSegment < bLastSegment) { - return -1; - } - if (aLastSegment > bLastSegment) { - return 1; - } - return 0; - }); - return resultArr; - }); - } - - private static filterOutdatedExtensions(extensions: IExtensionDescription[], targetPlatform: TargetPlatform): IExtensionDescription[] { - const result = new Map<string, IExtensionDescription>(); - for (const extension of extensions) { - const extensionKey = extension.identifier.value; - const existing = result.get(extensionKey); - if (existing) { - if (semver.gt(existing.version, extension.version)) { - continue; - } - if (semver.eq(existing.version, extension.version) && existing.targetPlatform === targetPlatform) { - continue; - } - } - result.set(extensionKey, extension); - } - return [...result.values()]; - } -} - -async function readFile(fileService: IFileService, filename: string): Promise<string> { - try { - const contents = await fileService.readFile(URI.file(filename), { atomic: true }); - return contents.value.toString(); - } catch (err) { - if (toFileOperationResult(err) === FileOperationResult.FILE_NOT_FOUND) { - const nodeLikeError = new Error(`File not found`); - (<any>nodeLikeError).code = 'ENOENT'; - throw nodeLikeError; - } - throw err; - } -} - -async function existsFile(fileService: IFileService, filename: string): Promise<boolean> { - try { - const stat = await fileService.resolve(URI.file(filename)); - return stat.isFile; - } catch (err) { - return false; - } -} - -async function readDirsInDir(fileService: IFileService, dirPath: string): Promise<string[]> { - const stat = await fileService.resolve(URI.file(dirPath)); - const result: string[] = []; - for (const child of (stat.children || [])) { - if (child.isDirectory) { - result.push(child.name); - } - } - return result; -} diff --git a/src/vs/workbench/services/extensions/common/extensions.ts b/src/vs/workbench/services/extensions/common/extensions.ts index 5ab3abcb921..722dba8dd6b 100644 --- a/src/vs/workbench/services/extensions/common/extensions.ts +++ b/src/vs/workbench/services/extensions/common/extensions.ts @@ -13,7 +13,7 @@ import { getExtensionId, getGalleryExtensionId } from 'vs/platform/extensionMana import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; import { ApiProposalName } from 'vs/workbench/services/extensions/common/extensionsApiProposals'; import { IV8Profile } from 'vs/platform/profiling/common/profiling'; -import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; +import { IExtensionDescriptionDelta } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; export const nullExtensionDescription = Object.freeze<IExtensionDescription>({ identifier: new ExtensionIdentifier('nullExtensionDescription'), @@ -150,10 +150,11 @@ export interface IExtensionHost { readonly remoteAuthority: string | null; readonly lazyStart: boolean; /** - * A collection of extensions that will execute or are executing on this extension host. + * A collection of extensions which includes information about which + * extension will execute or is executing on this extension host. * **NOTE**: this will reflect extensions correctly only after `start()` resolves. */ - readonly extensions: ExtensionDescriptionRegistry; + readonly extensions: ExtensionHostExtensions; readonly onExit: Event<[number, string | null]>; start(): Promise<IMessagePassingProtocol> | null; @@ -162,6 +163,201 @@ export interface IExtensionHost { dispose(): void; } +export class ExtensionHostExtensions { + + private _allExtensions: IExtensionDescription[]; + private _myExtensions: ExtensionIdentifier[]; + + constructor() { + this._allExtensions = []; + this._myExtensions = []; + } + + public toDelta(): IExtensionDescriptionDelta { + return { + toRemove: [], + toAdd: this._allExtensions, + myToRemove: [], + myToAdd: this._myExtensions + }; + } + + public set(allExtensions: IExtensionDescription[], myExtensions: ExtensionIdentifier[]): IExtensionDescriptionDelta { + const toRemove: ExtensionIdentifier[] = []; + const toAdd: IExtensionDescription[] = []; + const myToRemove: ExtensionIdentifier[] = []; + const myToAdd: ExtensionIdentifier[] = []; + + const oldExtensionsMap = extensionDescriptionArrayToMap(this._allExtensions); + const newExtensionsMap = extensionDescriptionArrayToMap(allExtensions); + const extensionsAreTheSame = (a: IExtensionDescription, b: IExtensionDescription) => { + return ( + (a.extensionLocation.toString() === b.extensionLocation.toString()) + || (a.isBuiltin === b.isBuiltin) + || (a.isUserBuiltin === b.isUserBuiltin) + || (a.isUnderDevelopment === b.isUnderDevelopment) + ); + }; + + for (const oldExtension of this._allExtensions) { + const newExtension = newExtensionsMap.get(ExtensionIdentifier.toKey(oldExtension.identifier)); + if (!newExtension) { + toRemove.push(oldExtension.identifier); + oldExtensionsMap.delete(ExtensionIdentifier.toKey(oldExtension.identifier)); + continue; + } + if (!extensionsAreTheSame(oldExtension, newExtension)) { + // The new extension is different than the old one + // (e.g. maybe it executes in a different location) + toRemove.push(oldExtension.identifier); + oldExtensionsMap.delete(ExtensionIdentifier.toKey(oldExtension.identifier)); + continue; + } + } + for (const newExtension of allExtensions) { + const oldExtension = oldExtensionsMap.get(ExtensionIdentifier.toKey(newExtension.identifier)); + if (!oldExtension) { + toAdd.push(newExtension); + continue; + } + if (!extensionsAreTheSame(oldExtension, newExtension)) { + // The new extension is different than the old one + // (e.g. maybe it executes in a different location) + toRemove.push(oldExtension.identifier); + oldExtensionsMap.delete(ExtensionIdentifier.toKey(oldExtension.identifier)); + continue; + } + } + + const myOldExtensionsSet = extensionIdentifiersArrayToSet(this._myExtensions); + const myNewExtensionsSet = extensionIdentifiersArrayToSet(myExtensions); + for (const oldExtensionId of this._myExtensions) { + if (!myNewExtensionsSet.has(ExtensionIdentifier.toKey(oldExtensionId))) { + myToRemove.push(oldExtensionId); + } + } + for (const newExtensionId of myExtensions) { + if (!myOldExtensionsSet.has(ExtensionIdentifier.toKey(newExtensionId))) { + myToAdd.push(newExtensionId); + } + } + + const delta = { toRemove, toAdd, myToRemove, myToAdd }; + this.delta(delta); + return delta; + } + + public delta(extensionsDelta: IExtensionDescriptionDelta): void { + const { toRemove, toAdd, myToRemove, myToAdd } = extensionsDelta; + // First handle removals + const toRemoveSet = extensionIdentifiersArrayToSet(toRemove); + const myToRemoveSet = extensionIdentifiersArrayToSet(myToRemove); + for (let i = 0; i < this._allExtensions.length; i++) { + if (toRemoveSet.has(ExtensionIdentifier.toKey(this._allExtensions[i].identifier))) { + this._allExtensions.splice(i, 1); + i--; + } + } + for (let i = 0; i < this._myExtensions.length; i++) { + if (myToRemoveSet.has(ExtensionIdentifier.toKey(this._myExtensions[i]))) { + this._myExtensions.splice(i, 1); + i--; + } + } + // Then handle additions + for (const extension of toAdd) { + this._allExtensions.push(extension); + } + for (const extensionId of myToAdd) { + this._myExtensions.push(extensionId); + } + } + + public containsExtension(extensionId: ExtensionIdentifier): boolean { + for (const myExtensionId of this._myExtensions) { + if (ExtensionIdentifier.equals(myExtensionId, extensionId)) { + return true; + } + } + return false; + } +} + +export class ExtensionIdentifierSet implements Set<ExtensionIdentifier> { + + readonly [Symbol.toStringTag]: string = 'ExtensionIdentifierSet'; + + private readonly _map = new Map<string, ExtensionIdentifier>(); + private readonly _toKey = ExtensionIdentifier.toKey; + + constructor(values?: Iterable<ExtensionIdentifier>) { + if (values) { + for (const value of values) { + this.add(value); + } + } + } + + get size(): number { + return this._map.size; + } + + add(value: ExtensionIdentifier): this { + this._map.set(this._toKey(value), value); + return this; + } + + clear(): void { + this._map.clear(); + } + + delete(value: ExtensionIdentifier): boolean { + return this._map.delete(this._toKey(value)); + } + + has(value: ExtensionIdentifier): boolean { + return this._map.has(this._toKey(value)); + } + + forEach(callbackfn: (value: ExtensionIdentifier, value2: ExtensionIdentifier, set: Set<ExtensionIdentifier>) => void, thisArg?: any): void { + this._map.forEach(value => callbackfn.call(thisArg, value, value, this)); + } + + *entries(): IterableIterator<[ExtensionIdentifier, ExtensionIdentifier]> { + for (let [_key, value] of this._map) { + yield [value, value]; + } + } + + keys(): IterableIterator<ExtensionIdentifier> { + return this._map.values(); + } + + values(): IterableIterator<ExtensionIdentifier> { + return this._map.values(); + } + + [Symbol.iterator](): IterableIterator<ExtensionIdentifier> { + return this._map.values(); + } +} + +export function extensionIdentifiersArrayToSet(extensionIds: ExtensionIdentifier[]): Set<string> { + const result = new Set<string>(); + for (const extensionId of extensionIds) { + result.add(ExtensionIdentifier.toKey(extensionId)); + } + return result; +} + +function extensionDescriptionArrayToMap(extensions: IExtensionDescription[]): Map<string, IExtensionDescription> { + const result = new Map<string, IExtensionDescription>(); + for (const extension of extensions) { + result.set(ExtensionIdentifier.toKey(extension.identifier), extension); + } + return result; +} + export function isProposedApiEnabled(extension: IExtensionDescription, proposal: ApiProposalName): boolean { if (!extension.enabledApiProposals) { return false; @@ -377,6 +573,8 @@ export function toExtension(extensionDescription: IExtensionDescription): IExten manifest: extensionDescription, location: extensionDescription.extensionLocation, targetPlatform: extensionDescription.targetPlatform, + validations: [], + isValid: true }; } diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index 0a1e93cded5..62beb7707f3 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -19,6 +19,7 @@ export const allApiProposals = Object.freeze({ documentFiltersExclusive: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.documentFiltersExclusive.d.ts', editorInsets: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.editorInsets.d.ts', extensionRuntime: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.extensionRuntime.d.ts', + extensionsAny: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.extensionsAny.d.ts', externalUriOpener: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.externalUriOpener.d.ts', fileSearchProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.fileSearchProvider.d.ts', findTextInFiles: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.findTextInFiles.d.ts', @@ -30,25 +31,23 @@ export const allApiProposals = Object.freeze({ inputBoxSeverity: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.inputBoxSeverity.d.ts', ipc: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.ipc.d.ts', notebookCellExecutionState: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookCellExecutionState.d.ts', - notebookConcatTextDocument: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookConcatTextDocument.d.ts', notebookContentProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookContentProvider.d.ts', notebookControllerKind: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookControllerKind.d.ts', notebookDebugOptions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookDebugOptions.d.ts', notebookDeprecated: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookDeprecated.d.ts', - notebookDocumentEvents: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookDocumentEvents.d.ts', notebookEditor: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookEditor.d.ts', notebookEditorDecorationType: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookEditorDecorationType.d.ts', notebookEditorEdit: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookEditorEdit.d.ts', notebookLiveShare: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookLiveShare.d.ts', notebookMessaging: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookMessaging.d.ts', notebookMime: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookMime.d.ts', + notebookProxyController: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookProxyController.d.ts', portsAttributes: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.portsAttributes.d.ts', quickPickSortByLabel: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.quickPickSortByLabel.d.ts', resolvers: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.resolvers.d.ts', scmActionButton: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmActionButton.d.ts', scmSelectedProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmSelectedProvider.d.ts', scmValidation: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmValidation.d.ts', - tabs: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.tabs.d.ts', taskPresentationGroup: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.taskPresentationGroup.d.ts', telemetry: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.telemetry.d.ts', terminalDataWriteEvent: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalDataWriteEvent.d.ts', diff --git a/src/vs/workbench/services/extensions/common/extensionsUtil.ts b/src/vs/workbench/services/extensions/common/extensionsUtil.ts index 4050fa6a460..977052ebcfc 100644 --- a/src/vs/workbench/services/extensions/common/extensionsUtil.ts +++ b/src/vs/workbench/services/extensions/common/extensionsUtil.ts @@ -4,16 +4,16 @@ *--------------------------------------------------------------------------------------------*/ import { ExtensionIdentifier, IExtensionDescription, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { ILog } from 'vs/workbench/services/extensions/common/extensionPoints'; import { localize } from 'vs/nls'; +import { ILogService } from 'vs/platform/log/common/log'; -export function dedupExtensions(system: IExtensionDescription[], user: IExtensionDescription[], development: IExtensionDescription[], log: ILog): IExtensionDescription[] { +export function dedupExtensions(system: IExtensionDescription[], user: IExtensionDescription[], development: IExtensionDescription[], logService: ILogService): IExtensionDescription[] { let result = new Map<string, IExtensionDescription>(); system.forEach((systemExtension) => { const extensionKey = ExtensionIdentifier.toKey(systemExtension.identifier); const extension = result.get(extensionKey); if (extension) { - log.warn(localize('overwritingExtension', "Overwriting extension {0} with {1}.", extension.extensionLocation.fsPath, systemExtension.extensionLocation.fsPath)); + logService.warn(localize('overwritingExtension', "Overwriting extension {0} with {1}.", extension.extensionLocation.fsPath, systemExtension.extensionLocation.fsPath)); } result.set(extensionKey, systemExtension); }); @@ -25,13 +25,13 @@ export function dedupExtensions(system: IExtensionDescription[], user: IExtensio // Overwriting a builtin extension inherits the `isBuiltin` property and it doesn't show a warning (<IRelaxedExtensionDescription>userExtension).isBuiltin = true; } else { - log.warn(localize('overwritingExtension', "Overwriting extension {0} with {1}.", extension.extensionLocation.fsPath, userExtension.extensionLocation.fsPath)); + logService.warn(localize('overwritingExtension', "Overwriting extension {0} with {1}.", extension.extensionLocation.fsPath, userExtension.extensionLocation.fsPath)); } } result.set(extensionKey, userExtension); }); development.forEach(developedExtension => { - log.info(localize('extensionUnderDevelopment', "Loading development extension at {0}", developedExtension.extensionLocation.fsPath)); + logService.info(localize('extensionUnderDevelopment', "Loading development extension at {0}", developedExtension.extensionLocation.fsPath)); const extensionKey = ExtensionIdentifier.toKey(developedExtension.identifier); const extension = result.get(extensionKey); if (extension) { diff --git a/src/vs/workbench/services/extensions/common/proxyIdentifier.ts b/src/vs/workbench/services/extensions/common/proxyIdentifier.ts index 6a02adb6cf1..42573f28c93 100644 --- a/src/vs/workbench/services/extensions/common/proxyIdentifier.ts +++ b/src/vs/workbench/services/extensions/common/proxyIdentifier.ts @@ -26,6 +26,8 @@ export interface IRPCProtocol { * Wait for the write buffer (if applicable) to become empty. */ drain(): Promise<void>; + + dispose(): void; } export class ProxyIdentifier<T> { diff --git a/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts b/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts index bd382d7f168..b6eb37647f8 100644 --- a/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts +++ b/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts @@ -25,10 +25,9 @@ import { ISignService } from 'vs/platform/sign/common/sign'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; import { parseExtensionDevOptions } from 'vs/workbench/services/extensions/common/extensionDevOptions'; import { createMessageOfType, isMessageOfType, MessageType, IExtensionHostInitData, UIKind } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; -import { ExtensionHostLogFileName, IExtensionHost, RemoteRunningLocation } from 'vs/workbench/services/extensions/common/extensions'; +import { ExtensionHostExtensions, ExtensionHostLogFileName, IExtensionHost, RemoteRunningLocation } from 'vs/workbench/services/extensions/common/extensions'; import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Extensions, IOutputChannelRegistry } from 'vs/workbench/services/output/common/output'; @@ -39,8 +38,8 @@ export interface IRemoteExtensionHostInitData { readonly extensionHostLogsPath: URI; readonly globalStorageHome: URI; readonly workspaceStorageHome: URI; - readonly extensions: IExtensionDescription[]; readonly allExtensions: IExtensionDescription[]; + readonly myExtensions: ExtensionIdentifier[]; } export interface IRemoteExtensionHostDataProvider { @@ -52,7 +51,7 @@ export class RemoteExtensionHost extends Disposable implements IExtensionHost { public readonly remoteAuthority: string; public readonly lazyStart = false; - public readonly extensions = new ExtensionDescriptionRegistry([]); + public readonly extensions = new ExtensionHostExtensions(); private _onExit: Emitter<[number, string | null]> = this._register(new Emitter<[number, string | null]>()); public readonly onExit: Event<[number, string | null]> = this._onExit.event; @@ -213,19 +212,8 @@ export class RemoteExtensionHost extends Disposable implements IExtensionHost { private async _createExtHostInitData(isExtensionDevelopmentDebug: boolean): Promise<IExtensionHostInitData> { const [telemetryInfo, remoteInitData] = await Promise.all([this._telemetryService.getTelemetryInfo(), this._initDataProvider.getInitData()]); - - // Collect all identifiers for extension ids which can be considered "resolved" - const remoteExtensions = new Set<string>(); - remoteInitData.extensions.forEach((extension) => remoteExtensions.add(ExtensionIdentifier.toKey(extension.identifier.value))); - - const resolvedExtensions = remoteInitData.allExtensions.filter(extension => !extension.main && !extension.browser).map(extension => extension.identifier); - const hostExtensions = ( - remoteInitData.allExtensions - .filter(extension => !remoteExtensions.has(ExtensionIdentifier.toKey(extension.identifier.value))) - .filter(extension => (extension.main || extension.browser) && extension.api === 'none').map(extension => extension.identifier) - ); const workspace = this._contextService.getWorkspace(); - this.extensions.deltaExtensions(remoteInitData.extensions, []); + const deltaExtensions = this.extensions.set(remoteInitData.allExtensions, remoteInitData.myExtensions); return { commit: this._productService.commit, version: this._productService.version, @@ -253,9 +241,8 @@ export class RemoteExtensionHost extends Disposable implements IExtensionHost { authority: this._initDataProvider.remoteAuthority, connectionData: remoteInitData.connectionData }, - resolvedExtensions: resolvedExtensions, - hostExtensions: hostExtensions, - extensions: this.extensions.getAllExtensionDescriptions(), + allExtensions: deltaExtensions.toAdd, + myExtensions: deltaExtensions.myToAdd, telemetryInfo, logLevel: this._logService.getLevel(), logsLocation: remoteInitData.extensionHostLogsPath, diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts index cfa6569db24..047e66bc6f5 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ILocalProcessExtensionHostDataProvider, LocalProcessExtensionHost } from 'vs/workbench/services/extensions/electron-browser/localProcessExtensionHost'; +import { ILocalProcessExtensionHostDataProvider, ILocalProcessExtensionHostInitData, LocalProcessExtensionHost } from 'vs/workbench/services/extensions/electron-browser/localProcessExtensionHost'; import { CachedExtensionScanner } from 'vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; @@ -35,7 +35,7 @@ import { IRemoteExplorerService } from 'vs/workbench/services/remote/common/remo import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { getRemoteName } from 'vs/platform/remote/common/remoteHosts'; import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment'; -import { IWebWorkerExtensionHostDataProvider, WebWorkerExtensionHost } from 'vs/workbench/services/extensions/browser/webWorkerExtensionHost'; +import { IWebWorkerExtensionHostDataProvider, IWebWorkerExtensionHostInitData, WebWorkerExtensionHost } from 'vs/workbench/services/extensions/browser/webWorkerExtensionHost'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { ILogService } from 'vs/platform/log/common/log'; import { CATEGORIES } from 'vs/workbench/common/actions'; @@ -145,7 +145,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten return this._remoteAgentService.scanSingleExtension(extension.location, extension.type === ExtensionType.System); } - return this._extensionScanner.scanSingleExtension(extension.location.fsPath, extension.type === ExtensionType.System, this._createLogger()); + return this._extensionScanner.scanSingleExtension(extension.location.fsPath, extension.type === ExtensionType.System); } private async _scanAllLocalExtensions(): Promise<IExtensionDescription[]> { @@ -157,23 +157,25 @@ export class ExtensionService extends AbstractExtensionService implements IExten private _createLocalExtensionHostDataProvider(isInitialStart: boolean, desiredRunningLocation: ExtensionRunningLocation): ILocalProcessExtensionHostDataProvider & IWebWorkerExtensionHostDataProvider { return { - getInitData: async () => { + getInitData: async (): Promise<ILocalProcessExtensionHostInitData & IWebWorkerExtensionHostInitData> => { if (isInitialStart) { // Here we load even extensions that would be disabled by workspace trust const localExtensions = this._checkEnabledAndProposedAPI(await this._scanAllLocalExtensions(), /* ignore workspace trust */true); const runningLocation = this._determineRunningLocation(localExtensions); - const localProcessExtensions = filterByRunningLocation(localExtensions, runningLocation, desiredRunningLocation); + const myExtensions = filterByRunningLocation(localExtensions, runningLocation, desiredRunningLocation); return { autoStart: false, - extensions: localProcessExtensions + allExtensions: localExtensions, + myExtensions: myExtensions.map(extension => extension.identifier) }; } else { // restart case const allExtensions = await this.getExtensions(); - const localProcessExtensions = this._filterByRunningLocation(allExtensions, desiredRunningLocation); + const myExtensions = this._filterByRunningLocation(allExtensions, desiredRunningLocation); return { autoStart: true, - extensions: localProcessExtensions + allExtensions: allExtensions, + myExtensions: myExtensions.map(extension => extension.identifier) }; } } @@ -431,7 +433,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten } protected async _scanAndHandleExtensions(): Promise<void> { - this._extensionScanner.startScanningExtensions(this._createLogger()); + this._extensionScanner.startScanningExtensions(); const remoteAuthority = this._environmentService.remoteAuthority; @@ -561,8 +563,8 @@ export class ExtensionService extends AbstractExtensionService implements IExten extensionHostLogsPath: remoteEnv.extensionHostLogsPath, globalStorageHome: remoteEnv.globalStorageHome, workspaceStorageHome: remoteEnv.workspaceStorageHome, - extensions: remoteExtensions, allExtensions: this._registry.getAllExtensionDescriptions(), + myExtensions: remoteExtensions.map(extension => extension.identifier), }); } @@ -583,7 +585,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten private _startExtensionHost(extensionHostManager: IExtensionHostManager, _extensions: IExtensionDescription[]): void { const extensions = this._filterByExtensionHostManager(_extensions, extensionHostManager); - extensionHostManager.start(extensions.map(extension => extension.identifier)); + extensionHostManager.start(this._registry.getAllExtensionDescriptions(), extensions.map(extension => extension.identifier)); } public _onExtensionHostExit(code: number): void { diff --git a/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts b/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts index 2f7e9b944f6..0f7f60bbc7d 100644 --- a/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts +++ b/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts @@ -4,14 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import { Server, Socket, createServer } from 'net'; -import { findFreePort } from 'vs/base/node/ports'; import { createRandomIPCHandle, NodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; import * as nls from 'vs/nls'; import { timeout } from 'vs/base/common/async'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { Emitter, Event } from 'vs/base/common/event'; -import { toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import * as objects from 'vs/base/common/objects'; import * as platform from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; @@ -30,11 +29,11 @@ import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; import { isUntitledWorkspace, IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { MessageType, createMessageOfType, isMessageOfType, IExtensionHostInitData, UIKind } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { withNullAsUndefined } from 'vs/base/common/types'; -import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { parseExtensionDevOptions } from '../common/extensionDevOptions'; import { VSBuffer } from 'vs/base/common/buffer'; import { IExtensionHostDebugService } from 'vs/platform/debug/common/extensionHostDebug'; -import { IExtensionHost, ExtensionHostLogFileName, LocalProcessRunningLocation } from 'vs/workbench/services/extensions/common/extensions'; +import { IExtensionHost, ExtensionHostLogFileName, LocalProcessRunningLocation, ExtensionHostExtensions } from 'vs/workbench/services/extensions/common/extensions'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { joinPath } from 'vs/base/common/resources'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -42,13 +41,14 @@ import { IOutputChannelRegistry, Extensions } from 'vs/workbench/services/output import { IShellEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/shellEnvironmentService'; import { IExtensionHostProcessOptions, IExtensionHostStarter } from 'vs/platform/extensions/common/extensionHostStarter'; import { SerializedError } from 'vs/base/common/errors'; -import { removeDangerousEnvVariables } from 'vs/base/node/processes'; +import { removeDangerousEnvVariables } from 'vs/base/common/processes'; import { StopWatch } from 'vs/base/common/stopwatch'; -import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; +import { process } from 'vs/base/parts/sandbox/electron-sandbox/globals'; export interface ILocalProcessExtensionHostInitData { readonly autoStart: boolean; - readonly extensions: IExtensionDescription[]; + readonly allExtensions: IExtensionDescription[]; + readonly myExtensions: ExtensionIdentifier[]; } export interface ILocalProcessExtensionHostDataProvider { @@ -108,7 +108,7 @@ export class LocalProcessExtensionHost implements IExtensionHost { public readonly remoteAuthority = null; public readonly lazyStart = false; - public readonly extensions = new ExtensionDescriptionRegistry([]); + public readonly extensions = new ExtensionHostExtensions(); private readonly _onExit: Emitter<[number, string]> = new Emitter<[number, string]>(); public readonly onExit: Event<[number, string]> = this._onExit.event; @@ -171,7 +171,7 @@ export class LocalProcessExtensionHost implements IExtensionHost { this._toDispose.add(this._onExit); this._toDispose.add(this._lifecycleService.onWillShutdown(e => this._onWillShutdown(e))); - this._toDispose.add(this._lifecycleService.onDidShutdown(reason => this.terminate())); + this._toDispose.add(this._lifecycleService.onDidShutdown(() => this.terminate())); this._toDispose.add(this._extensionHostDebugService.onClose(event => { if (this._isExtensionDevHost && this._environmentService.debugExtensionHost.debugId === event.sessionId) { this._nativeHostService.closeWindow(); @@ -182,12 +182,6 @@ export class LocalProcessExtensionHost implements IExtensionHost { this._hostService.reload(); } })); - - const globalExitListener = () => this.terminate(); - process.once('exit', globalExitListener); - this._toDispose.add(toDisposable(() => { - process.removeListener('exit' as 'loaded', globalExitListener); // https://github.com/electron/electron/issues/21475 - })); } public dispose(): void { @@ -380,7 +374,7 @@ export class LocalProcessExtensionHost implements IExtensionHost { } const expected = this._environmentService.debugExtensionHost.port; - const port = await findFreePort(expected, 10 /* try 10 ports */, 5000 /* try up to 5 seconds */, 2048 /* skip 2048 ports between attempts */); + const port = await this._nativeHostService.findFreePort(expected, 10 /* try 10 ports */, 5000 /* try up to 5 seconds */, 2048 /* skip 2048 ports between attempts */); if (!this._isExtensionDevTestFromCli) { if (!port) { @@ -504,7 +498,7 @@ export class LocalProcessExtensionHost implements IExtensionHost { private async _createExtHostInitData(): Promise<IExtensionHostInitData> { const [telemetryInfo, initData] = await Promise.all([this._telemetryService.getTelemetryInfo(), this._initDataProvider.getInitData()]); const workspace = this._contextService.getWorkspace(); - this.extensions.deltaExtensions(initData.extensions, []); + const deltaExtensions = this.extensions.set(initData.allExtensions, initData.myExtensions); return { commit: this._productService.commit, version: this._productService.version, @@ -533,9 +527,8 @@ export class LocalProcessExtensionHost implements IExtensionHost { connectionData: null, isRemote: false }, - resolvedExtensions: [], - hostExtensions: [], - extensions: this.extensions.getAllExtensionDescriptions(), + allExtensions: deltaExtensions.toAdd, + myExtensions: deltaExtensions.myToAdd, telemetryInfo, logLevel: this._logService.getLevel(), logsLocation: this._environmentService.extHostLogsPath, @@ -639,7 +632,7 @@ export class LocalProcessExtensionHost implements IExtensionHost { return withNullAsUndefined(this._inspectPort); } - public terminate(): void { + private terminate(): void { if (this._terminating) { return; } @@ -695,7 +688,7 @@ export class LocalProcessExtensionHost implements IExtensionHost { // to communicate this back to the main side to terminate the debug session if (this._isExtensionDevHost && !this._isExtensionDevTestFromCli && !this._isExtensionDevDebug && this._environmentService.debugExtensionHost.debugId) { this._extensionHostDebugService.terminateSession(this._environmentService.debugExtensionHost.debugId); - event.join(timeout(100 /* wait a bit for IPC to get delivered */), 'join.extensionDevelopment'); + event.join(timeout(100 /* wait a bit for IPC to get delivered */), { id: 'join.extensionDevelopment', label: nls.localize('join.extensionDevelopment', "Terminating extension debug session") }); } } } diff --git a/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts b/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts index c487b74c2b5..852ea7702e9 100644 --- a/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts +++ b/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts @@ -3,355 +3,80 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; import * as path from 'vs/base/common/path'; -import * as errors from 'vs/base/common/errors'; -import { FileAccess, Schemas } from 'vs/base/common/network'; -import * as objects from 'vs/base/common/objects'; import * as platform from 'vs/base/common/platform'; -import { joinPath, originalFSPath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; -import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; -import { BUILTIN_MANIFEST_CACHE_FILE, MANIFEST_CACHE_FOLDER, USER_MANIFEST_CACHE_FILE, IExtensionDescription, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { IProductService } from 'vs/platform/product/common/productService'; -import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; -import { IHostService } from 'vs/workbench/services/host/browser/host'; -import { Translations, ILog, ExtensionScanner, ExtensionScannerInput, IExtensionReference, IExtensionResolver } from 'vs/workbench/services/extensions/common/extensionPoints'; +import { IExtensionDescription, ExtensionType } from 'vs/platform/extensions/common/extensions'; import { dedupExtensions } from 'vs/workbench/services/extensions/common/extensionsUtil'; -import { IFileService } from 'vs/platform/files/common/files'; -import { VSBuffer } from 'vs/base/common/buffer'; -import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; - -interface IExtensionCacheData { - input: ExtensionScannerInput; - result: IExtensionDescription[]; -} - -let _SystemExtensionsRoot: string | null = null; -function getSystemExtensionsRoot(): string { - if (!_SystemExtensionsRoot) { - _SystemExtensionsRoot = path.normalize(path.join(FileAccess.asFileUri('', require).fsPath, '..', 'extensions')); - } - return _SystemExtensionsRoot; -} - -let _ExtraDevSystemExtensionsRoot: string | null = null; -function getExtraDevSystemExtensionsRoot(): string { - if (!_ExtraDevSystemExtensionsRoot) { - _ExtraDevSystemExtensionsRoot = path.normalize(path.join(FileAccess.asFileUri('', require).fsPath, '..', '.build', 'builtInExtensions')); - } - return _ExtraDevSystemExtensionsRoot; -} +import { IExtensionsScannerService, toExtensionDescription } from 'vs/platform/extensionManagement/common/extensionsScannerService'; +import { ILogService } from 'vs/platform/log/common/log'; +import Severity from 'vs/base/common/severity'; +import { localize } from 'vs/nls'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IHostService } from 'vs/workbench/services/host/browser/host'; +import { timeout } from 'vs/base/common/async'; export class CachedExtensionScanner { public readonly scannedExtensions: Promise<IExtensionDescription[]>; private _scannedExtensionsResolve!: (result: IExtensionDescription[]) => void; private _scannedExtensionsReject!: (err: any) => void; - public readonly translationConfig: Promise<Translations>; constructor( @INotificationService private readonly _notificationService: INotificationService, - @INativeWorkbenchEnvironmentService private readonly _environmentService: INativeWorkbenchEnvironmentService, @IHostService private readonly _hostService: IHostService, - @IProductService private readonly _productService: IProductService, - @IFileService private readonly _fileService: IFileService, - @IExtensionManagementService private readonly _extensionManagementService: IExtensionManagementService + @IExtensionsScannerService private readonly _extensionsScannerService: IExtensionsScannerService, + @ILogService private readonly _logService: ILogService, ) { this.scannedExtensions = new Promise<IExtensionDescription[]>((resolve, reject) => { this._scannedExtensionsResolve = resolve; this._scannedExtensionsReject = reject; }); - this.translationConfig = this._readTranslationConfig(); } - public async scanSingleExtension(path: string, isBuiltin: boolean, log: ILog): Promise<IExtensionDescription | null> { - const translations = await this.translationConfig; - - const version = this._productService.version; - const commit = this._productService.commit; - const date = this._productService.date; - const devMode = !this._environmentService.isBuilt; - const locale = platform.language; - const targetPlatform = await this._extensionManagementService.getTargetPlatform(); - const input = new ExtensionScannerInput(version, date, commit, locale, devMode, path, isBuiltin, false, targetPlatform, translations); - return ExtensionScanner.scanSingleExtension(input, log, this._fileService); + public async scanSingleExtension(extensionPath: string, isBuiltin: boolean): Promise<IExtensionDescription | null> { + const scannedExtension = await this._extensionsScannerService.scanExistingExtension(URI.file(path.resolve(extensionPath)), isBuiltin ? ExtensionType.System : ExtensionType.User, { language: platform.language }); + return scannedExtension ? toExtensionDescription(scannedExtension, false) : null; } - public async startScanningExtensions(log: ILog): Promise<void> { + public async startScanningExtensions(): Promise<void> { try { - const translations = await this.translationConfig; - const { system, user, development } = await this._scanInstalledExtensions(log, translations); - const r = dedupExtensions(system, user, development, log); + const { system, user, development } = await this._scanInstalledExtensions(); + const r = dedupExtensions(system, user, development, this._logService); this._scannedExtensionsResolve(r); } catch (err) { this._scannedExtensionsReject(err); } } - private async _validateExtensionsCache(cacheKey: string, input: ExtensionScannerInput): Promise<void> { - const cacheFolder = path.join(this._environmentService.userDataPath, MANIFEST_CACHE_FOLDER); - const cacheFile = path.join(cacheFolder, cacheKey); - - const expected = JSON.parse(JSON.stringify(await ExtensionScanner.scanExtensions(input, new NullLogger(), this._fileService))); - - const cacheContents = await this._readExtensionCache(cacheKey); - if (!cacheContents) { - // Cache has been deleted by someone else, which is perfectly fine... - return; - } - const actual = cacheContents.result; - - if (objects.equals(expected, actual)) { - // Cache is valid and running with it is perfectly fine... - return; - } - - try { - await this._fileService.del(URI.file(cacheFile)); - } catch (err) { - errors.onUnexpectedError(err); - console.error(err); - } - - this._notificationService.prompt( - Severity.Error, - nls.localize('extensionCache.invalid', "Extensions have been modified on disk. Please reload the window."), - [{ - label: nls.localize('reloadWindow', "Reload Window"), - run: () => this._hostService.reload() - }] - ); - } - - private async _readExtensionCache(cacheKey: string): Promise<IExtensionCacheData | null> { - const cacheFolder = path.join(this._environmentService.userDataPath, MANIFEST_CACHE_FOLDER); - const cacheFile = path.join(cacheFolder, cacheKey); - + private async _scanInstalledExtensions(): Promise<{ system: IExtensionDescription[]; user: IExtensionDescription[]; development: IExtensionDescription[] }> { try { - const cacheRawContents = await this._fileService.readFile(URI.file(cacheFile)); - return JSON.parse(cacheRawContents.value.toString()); - } catch (err) { - // That's ok... - } - - return null; - } - - private async _writeExtensionCache(cacheKey: string, cacheContents: IExtensionCacheData): Promise<void> { - const cacheFolder = path.join(this._environmentService.userDataPath, MANIFEST_CACHE_FOLDER); - const cacheFile = path.join(cacheFolder, cacheKey); - - try { - await this._fileService.createFolder(URI.file(cacheFolder)); - } catch (err) { - // That's ok... - } - - try { - await this._fileService.writeFile(URI.file(cacheFile), VSBuffer.fromString(JSON.stringify(cacheContents))); - } catch (err) { - // That's ok... - } - } - - private async _scanExtensionsWithCache(cacheKey: string, input: ExtensionScannerInput, log: ILog): Promise<IExtensionDescription[]> { - if (input.devMode) { - // Do not cache when running out of sources... - return ExtensionScanner.scanExtensions(input, log, this._fileService); - } - - try { - const folderStat = await this._fileService.stat(URI.file(input.absoluteFolderPath)); - if (typeof folderStat.mtime === 'number') { - input.mtime = folderStat.mtime; - } - } catch (err) { - // That's ok... - } - - const cacheContents = await this._readExtensionCache(cacheKey); - if (cacheContents && cacheContents.input && ExtensionScannerInput.equals(cacheContents.input, input)) { - // Validate the cache asynchronously after 5s - setTimeout(async () => { - try { - await this._validateExtensionsCache(cacheKey, input); - } catch (err) { - errors.onUnexpectedError(err); - } - }, 5000); - return cacheContents.result.map((extensionDescription) => { - // revive URI object - (<IRelaxedExtensionDescription>extensionDescription).extensionLocation = URI.revive(extensionDescription.extensionLocation); - return extensionDescription; - }); - } - - const counterLogger = new CounterLogger(log); - const result = await ExtensionScanner.scanExtensions(input, counterLogger, this._fileService); - if (counterLogger.errorCnt === 0) { - // Nothing bad happened => cache the result - const cacheContents: IExtensionCacheData = { - input: input, - result: result - }; - await this._writeExtensionCache(cacheKey, cacheContents); - } - - return result; - } - - private async _readTranslationConfig(): Promise<Translations> { - if (platform.translationsConfigFile) { - try { - const content = await this._fileService.readFile(URI.file(platform.translationsConfigFile)); - return JSON.parse(content.value.toString()) as Translations; - } catch (err) { - // no problemo - } - } - return Object.create(null); - } - - private async _scanInstalledExtensions( - log: ILog, - translations: Translations - ): Promise<{ system: IExtensionDescription[]; user: IExtensionDescription[]; development: IExtensionDescription[] }> { - - const version = this._productService.version; - const commit = this._productService.commit; - const date = this._productService.date; - const devMode = !this._environmentService.isBuilt; - const locale = platform.language; - const targetPlatform = await this._extensionManagementService.getTargetPlatform(); - - const builtinExtensions = this._scanExtensionsWithCache( - BUILTIN_MANIFEST_CACHE_FILE, - new ExtensionScannerInput(version, date, commit, locale, devMode, getSystemExtensionsRoot(), true, false, targetPlatform, translations), - log - ); - - let finalBuiltinExtensions: Promise<IExtensionDescription[]> = builtinExtensions; - - if (devMode) { - const builtInExtensions = Promise.resolve<IBuiltInExtension[]>(this._productService.builtInExtensions || []); - - const controlFilePath = joinPath(this._environmentService.userHome, '.vscode-oss-dev', 'extensions', 'control.json').fsPath; - const controlFile = this._fileService.readFile(URI.file(controlFilePath)) - .then<IBuiltInExtensionControl>(raw => JSON.parse(raw.value.toString()), () => ({} as any)); - - const input = new ExtensionScannerInput(version, date, commit, locale, devMode, getExtraDevSystemExtensionsRoot(), true, false, targetPlatform, translations); - const extraBuiltinExtensions = Promise.all([builtInExtensions, controlFile]) - .then(([builtInExtensions, control]) => new ExtraBuiltInExtensionResolver(builtInExtensions, control)) - .then(resolver => ExtensionScanner.scanExtensions(input, log, this._fileService, resolver)); - - finalBuiltinExtensions = ExtensionScanner.mergeBuiltinExtensions(builtinExtensions, extraBuiltinExtensions); - } - - const userExtensions = (this._scanExtensionsWithCache( - USER_MANIFEST_CACHE_FILE, - new ExtensionScannerInput(version, date, commit, locale, devMode, this._environmentService.extensionsPath, false, false, targetPlatform, translations), - log - )); - - // Always load developed extensions while extensions development - let developedExtensions: Promise<IExtensionDescription[]> = Promise.resolve([]); - if (this._environmentService.isExtensionDevelopment && this._environmentService.extensionDevelopmentLocationURI) { - const extDescsP = this._environmentService.extensionDevelopmentLocationURI.filter(extLoc => extLoc.scheme === Schemas.file).map(extLoc => { - return ExtensionScanner.scanOneOrMultipleExtensions( - new ExtensionScannerInput(version, date, commit, locale, devMode, originalFSPath(extLoc), false, true, targetPlatform, translations), - log, - this._fileService + const language = platform.language; + const [scannedSystemExtensions, scannedUserExtensions] = await Promise.all([ + this._extensionsScannerService.scanSystemExtensions({ language, useCache: true, checkControlFile: true }), + this._extensionsScannerService.scanUserExtensions({ language, useCache: true })]); + const scannedDevelopedExtensions = await this._extensionsScannerService.scanExtensionsUnderDevelopment({ language }, [...scannedSystemExtensions, ...scannedUserExtensions]); + const system = scannedSystemExtensions.map(e => toExtensionDescription(e, false)); + const user = scannedUserExtensions.map(e => toExtensionDescription(e, false)); + const development = scannedDevelopedExtensions.map(e => toExtensionDescription(e, true)); + const disposable = this._extensionsScannerService.onDidChangeCache(() => { + disposable.dispose(); + this._notificationService.prompt( + Severity.Error, + localize('extensionCache.invalid', "Extensions have been modified on disk. Please reload the window."), + [{ + label: localize('reloadWindow', "Reload Window"), + run: () => this._hostService.reload() + }] ); }); - developedExtensions = Promise.all(extDescsP).then((extDescArrays: IExtensionDescription[][]) => { - let extDesc: IExtensionDescription[] = []; - for (let eds of extDescArrays) { - extDesc = extDesc.concat(eds); - } - return extDesc; - }); - } - - return Promise.all([finalBuiltinExtensions, userExtensions, developedExtensions]).then((extensionDescriptions: IExtensionDescription[][]) => { - const system = extensionDescriptions[0]; - const user = extensionDescriptions[1]; - const development = extensionDescriptions[2]; + timeout(5000).then(() => disposable.dispose()); return { system, user, development }; - }).then(undefined, err => { - log.error(`Error scanning installed extensions:`); - log.error(err); + } catch (err) { + this._logService.error(`Error scanning installed extensions:`); + this._logService.error(err); return { system: [], user: [], development: [] }; - }); - } -} - -interface IBuiltInExtension { - name: string; - version: string; - repo: string; -} - -interface IBuiltInExtensionControl { - [name: string]: 'marketplace' | 'disabled' | string; -} - -class ExtraBuiltInExtensionResolver implements IExtensionResolver { - - constructor(private builtInExtensions: IBuiltInExtension[], private control: IBuiltInExtensionControl) { } - - resolveExtensions(): Promise<IExtensionReference[]> { - const result: IExtensionReference[] = []; - - for (const ext of this.builtInExtensions) { - const controlState = this.control[ext.name] || 'marketplace'; - - switch (controlState) { - case 'disabled': - break; - case 'marketplace': - result.push({ name: ext.name, path: path.join(getExtraDevSystemExtensionsRoot(), ext.name) }); - break; - default: - result.push({ name: ext.name, path: controlState }); - break; - } } - - return Promise.resolve(result); - } -} - -class CounterLogger implements ILog { - - public errorCnt = 0; - public warnCnt = 0; - public infoCnt = 0; - - constructor(private readonly _actual: ILog) { - } - - public error(message: string | Error): void { - this.errorCnt++; - this._actual.error(message); - } - - public warn(message: string): void { - this.warnCnt++; - this._actual.warn(message); } - public info(message: string): void { - this.infoCnt++; - this._actual.info(message); - } -} - -class NullLogger implements ILog { - public error(message: string | Error): void { - } - public warn(message: string): void { - } - public info(message: string): void { - } } diff --git a/src/vs/workbench/services/host/browser/browserHostService.ts b/src/vs/workbench/services/host/browser/browserHostService.ts index cdc63895e27..955ba20bddf 100644 --- a/src/vs/workbench/services/host/browser/browserHostService.ts +++ b/src/vs/workbench/services/host/browser/browserHostService.ts @@ -34,6 +34,7 @@ import { isUndefined } from 'vs/base/common/types'; import { isTemporaryWorkspace, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { Schemas } from 'vs/base/common/network'; +import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; /** * A workspace to open in the workbench can either be: @@ -285,14 +286,16 @@ export class BrowserHostService extends Disposable implements IHostService { // Same Window: open via editor service in current window if (this.shouldReuse(options, true /* file */)) { - let openables: IPathData[] = []; + let openables: IPathData<ITextEditorOptions>[] = []; // Support: --goto parameter to open on line/col if (options?.gotoLineMode) { const pathColumnAware = parseLineAndColumnAware(openable.fileUri.path); openables = [{ fileUri: openable.fileUri.with({ path: pathColumnAware.path }), - selection: !isUndefined(pathColumnAware.line) ? { startLineNumber: pathColumnAware.line, startColumn: pathColumnAware.column || 1 } : undefined + options: { + selection: !isUndefined(pathColumnAware.line) ? { startLineNumber: pathColumnAware.line, startColumn: pathColumnAware.column || 1 } : undefined + } }]; } else { openables = [openable]; diff --git a/src/vs/workbench/services/hover/browser/hover.ts b/src/vs/workbench/services/hover/browser/hover.ts index c22832f9755..074b8203b5e 100644 --- a/src/vs/workbench/services/hover/browser/hover.ts +++ b/src/vs/workbench/services/hover/browser/hover.ts @@ -33,7 +33,8 @@ export interface IHoverService { showHover(options: IHoverOptions, focus?: boolean): IHoverWidget | undefined; /** - * Hides the hover if it was visible. + * Hides the hover if it was visible. This call will be ignored if the the hover is currently + * "locked" via the alt/option key. */ hideHover(): void; } diff --git a/src/vs/workbench/services/hover/browser/hoverService.ts b/src/vs/workbench/services/hover/browser/hoverService.ts index 7d5331927d9..f0a3c2487f4 100644 --- a/src/vs/workbench/services/hover/browser/hoverService.ts +++ b/src/vs/workbench/services/hover/browser/hoverService.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/hover'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { editorHoverBackground, editorHoverBorder, textLinkForeground, editorHoverForeground, editorHoverStatusBarBackground, textCodeBlockBackground, widgetShadow, textLinkActiveForeground } from 'vs/platform/theme/common/colorRegistry'; +import { editorHoverBackground, editorHoverBorder, textLinkForeground, editorHoverForeground, editorHoverStatusBarBackground, textCodeBlockBackground, widgetShadow, textLinkActiveForeground, focusBorder, toolbarHoverBackground } from 'vs/platform/theme/common/colorRegistry'; import { IHoverService, IHoverOptions, IHoverWidget } from 'vs/workbench/services/hover/browser/hover'; import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -19,6 +19,7 @@ export class HoverService implements IHoverService { declare readonly _serviceBrand: undefined; private _currentHoverOptions: IHoverOptions | undefined; + private _currentHover: HoverWidget | undefined; constructor( @IInstantiationService private readonly _instantiationService: IInstantiationService, @@ -50,6 +51,13 @@ export class HoverService implements IHoverService { } else { hoverDisposables.add(addDisposableListener(options.target, EventType.CLICK, () => this.hideHover())); } + const focusedElement = <HTMLElement | null>document.activeElement; + if (focusedElement) { + hoverDisposables.add(addDisposableListener(focusedElement, EventType.KEY_DOWN, e => this._keyDown(e, hover))); + hoverDisposables.add(addDisposableListener(document, EventType.KEY_DOWN, e => this._keyDown(e, hover))); + hoverDisposables.add(addDisposableListener(focusedElement, EventType.KEY_UP, e => this._keyUp(e, hover))); + hoverDisposables.add(addDisposableListener(document, EventType.KEY_UP, e => this._keyUp(e, hover))); + } if (options.hideOnKeyDown) { const focusedElement = document.activeElement; if (focusedElement) { @@ -64,13 +72,16 @@ export class HoverService implements IHoverService { hoverDisposables.add(toDisposable(() => observer.disconnect())); } + this._currentHover = hover; + return hover; } hideHover(): void { - if (!this._currentHoverOptions) { + if (this._currentHover?.isLocked || !this._currentHoverOptions) { return; } + this._currentHover = undefined; this._currentHoverOptions = undefined; this._contextViewService.hideContextView(); } @@ -81,6 +92,24 @@ export class HoverService implements IHoverService { hover.dispose(); } } + + private _keyDown(e: KeyboardEvent, hover: HoverWidget) { + if (e.key === 'Alt') { + hover.isLocked = true; + return; + } + this.hideHover(); + } + + private _keyUp(e: KeyboardEvent, hover: HoverWidget) { + if (e.key === 'Alt') { + hover.isLocked = false; + // Hide if alt is released while the mouse os not over hover/target + if (!hover.isMouseIn) { + this.hideHover(); + } + } + } } class HoverContextViewDelegate implements IDelegate { @@ -126,6 +155,8 @@ registerThemingParticipant((theme, collector) => { const hoverBorder = theme.getColor(editorHoverBorder); if (hoverBorder) { collector.addRule(`.monaco-workbench .workbench-hover { border: 1px solid ${hoverBorder}; }`); + collector.addRule(`.monaco-workbench .workbench-hover-container.locked .workbench-hover { outline: 1px solid ${hoverBorder}; }`); + collector.addRule(`.monaco-workbench .workbench-hover .hover-row:not(:first-child):not(:empty) { border-top: 1px solid ${hoverBorder.transparent(0.5)}; }`); collector.addRule(`.monaco-workbench .workbench-hover hr { border-top: 1px solid ${hoverBorder.transparent(0.5)}; }`); collector.addRule(`.monaco-workbench .workbench-hover hr { border-bottom: 0px solid ${hoverBorder.transparent(0.5)}; }`); @@ -133,6 +164,15 @@ registerThemingParticipant((theme, collector) => { collector.addRule(`.monaco-workbench .workbench-hover-pointer:after { border-right: 1px solid ${hoverBorder}; }`); collector.addRule(`.monaco-workbench .workbench-hover-pointer:after { border-bottom: 1px solid ${hoverBorder}; }`); } + const focus = theme.getColor(focusBorder); + if (focus) { + collector.addRule(`.monaco-workbench .workbench-hover-container.locked .workbench-hover:focus { outline-color: ${focus}; }`); + collector.addRule(`.monaco-workbench .workbench-hover-lock:focus { outline: 1px solid ${focus}; }`); + } + const toolbarHoverBackgroundColor = theme.getColor(toolbarHoverBackground); + if (toolbarHoverBackgroundColor) { + collector.addRule(`.monaco-workbench .workbench-hover-container.locked .workbench-hover-lock:hover { background-color: ${toolbarHoverBackgroundColor}; }`); + } const link = theme.getColor(textLinkForeground); if (link) { collector.addRule(`.monaco-workbench .workbench-hover a { color: ${link}; }`); diff --git a/src/vs/workbench/services/hover/browser/hoverWidget.ts b/src/vs/workbench/services/hover/browser/hoverWidget.ts index 7856afda3a1..837a4bdfd52 100644 --- a/src/vs/workbench/services/hover/browser/hoverWidget.ts +++ b/src/vs/workbench/services/hover/browser/hoverWidget.ts @@ -38,7 +38,7 @@ const enum Constants { export class HoverWidget extends Widget { private readonly _messageListeners = new DisposableStore(); - private readonly _mouseTracker: CompositeMouseTracker; + private readonly _lockMouseTracker: CompositeMouseTracker; private readonly _hover: BaseHoverWidget; private readonly _hoverPointer: HTMLElement | undefined; @@ -51,8 +51,10 @@ export class HoverWidget extends Widget { private _forcePosition: boolean = false; private _x: number = 0; private _y: number = 0; + private _isLocked: boolean = false; get isDisposed(): boolean { return this._isDisposed; } + get isMouseIn(): boolean { return this._lockMouseTracker.isMouseIn; } get domNode(): HTMLElement { return this._hover.containerDomNode; } private readonly _onDispose = this._register(new Emitter<void>()); @@ -64,6 +66,19 @@ export class HoverWidget extends Widget { get x(): number { return this._x; } get y(): number { return this._y; } + /** + * Whether the hover is "locked" by holding the alt/option key. When locked, the hover will not + * hide and can be hovered regardless of whether the `hideOnHover` hover option is set. + */ + get isLocked(): boolean { return this._isLocked; } + set isLocked(value: boolean) { + if (this._isLocked === value) { + return; + } + this._isLocked = value; + this._hoverContainer.classList.toggle('locked', this._isLocked); + } + constructor( options: IHoverOptions, @IKeybindingService private readonly _keybindingService: IKeybindingService, @@ -165,7 +180,6 @@ export class HoverWidget extends Widget { } this._hoverContainer.appendChild(this._hover.containerDomNode); - const mouseTrackerTargets = [...this._target.targetElements]; let hideOnHover: boolean; if (options.actions && options.actions.length > 0) { // If there are actions, require hover so they can be accessed @@ -179,12 +193,31 @@ export class HoverWidget extends Widget { hideOnHover = options.hideOnHover; } } + const mouseTrackerTargets = [...this._target.targetElements]; if (!hideOnHover) { mouseTrackerTargets.push(this._hoverContainer); } - this._mouseTracker = new CompositeMouseTracker(mouseTrackerTargets); - this._register(this._mouseTracker.onMouseOut(() => this.dispose())); - this._register(this._mouseTracker); + const mouseTracker = this._register(new CompositeMouseTracker(mouseTrackerTargets)); + this._register(mouseTracker.onMouseOut(() => { + if (!this._isLocked) { + this.dispose(); + } + })); + + // Setup another mouse tracker when hideOnHover is set in order to track the hover as well + // when it is locked. This ensures the hover will hide on mouseout after alt has been + // released to unlock the element. + if (hideOnHover) { + const mouseTracker2Targets = [...this._target.targetElements, this._hoverContainer]; + this._lockMouseTracker = this._register(new CompositeMouseTracker(mouseTracker2Targets)); + this._register(this._lockMouseTracker.onMouseOut(() => { + if (!this._isLocked) { + this.dispose(); + } + })); + } else { + this._lockMouseTracker = mouseTracker; + } } public render(container: HTMLElement): void { @@ -468,6 +501,8 @@ class CompositeMouseTracker extends Widget { private readonly _onMouseOut = this._register(new Emitter<void>()); get onMouseOut(): Event<void> { return this._onMouseOut.event; } + get isMouseIn(): boolean { return this._isMouseIn; } + constructor( private _elements: HTMLElement[] ) { diff --git a/src/vs/workbench/services/hover/browser/media/hover.css b/src/vs/workbench/services/hover/browser/media/hover.css index 215cb9b9fcc..d560a4a3a96 100644 --- a/src/vs/workbench/services/hover/browser/media/hover.css +++ b/src/vs/workbench/services/hover/browser/media/hover.css @@ -38,6 +38,12 @@ width: 5px; height: 5px; } +.monaco-workbench .locked .workbench-hover-pointer:after { + width: 4px; + height: 4px; + border-right-width: 2px; + border-bottom-width: 2px; +} .monaco-workbench .workbench-hover-pointer.left { left: -3px; } .monaco-workbench .workbench-hover-pointer.right { right: 3px; } diff --git a/src/vs/workbench/services/label/common/labelService.ts b/src/vs/workbench/services/label/common/labelService.ts index dafc93d58a3..1f6860f0050 100644 --- a/src/vs/workbench/services/label/common/labelService.ts +++ b/src/vs/workbench/services/label/common/labelService.ts @@ -6,11 +6,11 @@ import { localize } from 'vs/nls'; import { URI } from 'vs/base/common/uri'; import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; -import * as paths from 'vs/base/common/path'; +import { posix, win32 } from 'vs/base/common/path'; import { Emitter } from 'vs/base/common/event'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry, IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { Registry } from 'vs/platform/registry/common/platform'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IWorkspaceContextService, IWorkspace, isWorkspace, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IWorkspaceIdentifier, toWorkspaceIdentifier, WORKSPACE_EXTENSION, isUntitledWorkspace, isTemporaryWorkspace } from 'vs/platform/workspace/common/workspace'; import { basenameOrAuthority, basename, joinPath, dirname } from 'vs/base/common/resources'; import { tildify, getPathLabel } from 'vs/base/common/labels'; @@ -21,6 +21,9 @@ import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; +import { OperatingSystem, OS } from 'vs/base/common/platform'; +import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { Schemas } from 'vs/base/common/network'; const resourceLabelFormattersExtPoint = ExtensionsRegistry.registerExtensionPoint<ResourceLabelFormatter[]>({ extensionPoint: 'resourceLabelFormatters', @@ -78,17 +81,19 @@ function hasDriveLetterIgnorePlatform(path: string): boolean { } class ResourceLabelFormattersHandler implements IWorkbenchContribution { - private formattersDisposables = new Map<ResourceLabelFormatter, IDisposable>(); + + private readonly formattersDisposables = new Map<ResourceLabelFormatter, IDisposable>(); constructor(@ILabelService labelService: ILabelService) { resourceLabelFormattersExtPoint.setHandler((extensions, delta) => { delta.added.forEach(added => added.value.forEach(formatter => { if (!isProposedApiEnabled(added.description, 'contribLabelFormatterWorkspaceTooltip') && formatter.formatting.workspaceTooltip) { - // workspaceTooltip is only proposed - formatter.formatting.workspaceTooltip = undefined; + formatter.formatting.workspaceTooltip = undefined; // workspaceTooltip is only proposed } + this.formattersDisposables.set(formatter, labelService.registerFormatter(formatter)); })); + delta.removed.forEach(removed => removed.value.forEach(formatter => { this.formattersDisposables.get(formatter)!.dispose(); })); @@ -106,37 +111,70 @@ export class LabelService extends Disposable implements ILabelService { private readonly _onDidChangeFormatters = this._register(new Emitter<IFormatterChangeEvent>({ leakWarningThreshold: 400 })); readonly onDidChangeFormatters = this._onDidChangeFormatters.event; + private os: OperatingSystem; + private userHome: URI | undefined; + constructor( - @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, - @IPathService private readonly pathService: IPathService + @IPathService private readonly pathService: IPathService, + @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService ) { super(); + + // Find some meaningful defaults until the remote environment + // is resolved, by taking the current OS we are running in + // and by taking the local `userHome` if we run on a local + // file scheme. + this.os = OS; + this.userHome = pathService.defaultUriScheme === Schemas.file ? this.pathService.userHome({ preferLocal: true }) : undefined; + + // Remote environment is potentially long running + this.resolveRemoteEnvironment(); + } + + private async resolveRemoteEnvironment(): Promise<void> { + + // OS + const env = await this.remoteAgentService.getEnvironment(); + this.os = env?.os ?? OS; + + // User home + this.userHome = await this.pathService.userHome(); } findFormatting(resource: URI): ResourceLabelFormatting | undefined { let bestResult: ResourceLabelFormatter | undefined; - this.formatters.forEach(formatter => { + for (const formatter of this.formatters) { if (formatter.scheme === resource.scheme) { if (!formatter.authority && (!bestResult || formatter.priority)) { bestResult = formatter; - return; + continue; } + if (!formatter.authority) { - return; + continue; } - if (match(formatter.authority.toLowerCase(), resource.authority.toLowerCase()) && (!bestResult || !bestResult.authority || formatter.authority.length > bestResult.authority.length || ((formatter.authority.length === bestResult.authority.length) && formatter.priority))) { + if ( + match(formatter.authority.toLowerCase(), resource.authority.toLowerCase()) && + ( + !bestResult || + !bestResult.authority || + formatter.authority.length > bestResult.authority.length || + ((formatter.authority.length === bestResult.authority.length) && formatter.priority) + ) + ) { bestResult = formatter; } } - }); + } return bestResult ? bestResult.formatting : undefined; } - getUriLabel(resource: URI, options: { relative?: boolean; noPrefix?: boolean; endWithSeparator?: boolean; separator?: '/' | '\\' } = {}): string { + getUriLabel(resource: URI, options: { relative?: boolean; noPrefix?: boolean; separator?: '/' | '\\' } = {}): string { let formatting = this.findFormatting(resource); if (formatting && options.separator) { // mixin separator if defined from the outside @@ -154,51 +192,66 @@ export class LabelService extends Disposable implements ILabelService { return label; } - private doGetUriLabel(resource: URI, formatting?: ResourceLabelFormatting, options: { relative?: boolean; noPrefix?: boolean; endWithSeparator?: boolean } = {}): string { + private doGetUriLabel(resource: URI, formatting?: ResourceLabelFormatting, options: { relative?: boolean; noPrefix?: boolean } = {}): string { if (!formatting) { - return getPathLabel(resource.path, { userHome: this.pathService.resolvedUserHome }, options.relative ? this.contextService : undefined); + return getPathLabel(resource, { + os: this.os, + tildify: this.userHome ? { userHome: this.userHome } : undefined, + relative: options.relative ? { + noPrefix: options.noPrefix, + getWorkspace: () => this.contextService.getWorkspace(), + getWorkspaceFolder: resource => this.contextService.getWorkspaceFolder(resource) + } : undefined + }); } - let label: string | undefined; - const baseResource = this.contextService?.getWorkspaceFolder(resource); + // Relative label + if (options.relative) { + const folder = this.contextService?.getWorkspaceFolder(resource); + if (folder) { + const folderLabel = this.formatUri(folder.uri, formatting, options.noPrefix); - if (options.relative && baseResource) { - const baseResourceLabel = this.formatUri(baseResource.uri, formatting, options.noPrefix); - let relativeLabel = this.formatUri(resource, formatting, options.noPrefix); + let relativeLabel = this.formatUri(resource, formatting, options.noPrefix); + let overlap = 0; + while (relativeLabel[overlap] && relativeLabel[overlap] === folderLabel[overlap]) { + overlap++; + } - let overlap = 0; - while (relativeLabel[overlap] && relativeLabel[overlap] === baseResourceLabel[overlap]) { overlap++; } - if (!relativeLabel[overlap] || relativeLabel[overlap] === formatting.separator) { - relativeLabel = relativeLabel.substring(1 + overlap); - } else if (overlap === baseResourceLabel.length && baseResource.uri.path === '/') { - relativeLabel = relativeLabel.substring(overlap); - } + if (!relativeLabel[overlap] || relativeLabel[overlap] === formatting.separator) { + relativeLabel = relativeLabel.substring(1 + overlap); + } else if (overlap === folderLabel.length && folder.uri.path === posix.sep) { + relativeLabel = relativeLabel.substring(overlap); + } - const hasMultipleRoots = this.contextService.getWorkspace().folders.length > 1; - if (hasMultipleRoots && !options.noPrefix) { - const rootName = baseResource?.name ?? basenameOrAuthority(baseResource.uri); - relativeLabel = relativeLabel ? (rootName + ' • ' + relativeLabel) : rootName; // always show root basename if there are multiple - } + // always show root basename if there are multiple folders + const hasMultipleRoots = this.contextService.getWorkspace().folders.length > 1; + if (hasMultipleRoots && !options.noPrefix) { + const rootName = folder?.name ?? basenameOrAuthority(folder.uri); + relativeLabel = relativeLabel ? `${rootName} • ${relativeLabel}` : rootName; + } - label = relativeLabel; - } else { - label = this.formatUri(resource, formatting, options.noPrefix); + return relativeLabel; + } } - return options.endWithSeparator ? this.appendSeparatorIfMissing(label, formatting) : label; + // Absolute label + return this.formatUri(resource, formatting, options.noPrefix); } getUriBasenameLabel(resource: URI): string { const formatting = this.findFormatting(resource); const label = this.doGetUriLabel(resource, formatting); - if (formatting) { - switch (formatting.separator) { - case paths.win32.sep: return paths.win32.basename(label); - case paths.posix.sep: return paths.posix.basename(label); - } + + let pathLib: typeof win32 | typeof posix; + if (formatting?.separator === win32.sep) { + pathLib = win32; + } else if (formatting?.separator === posix.sep) { + pathLib = posix; + } else { + pathLib = (this.os === OperatingSystem.Windows) ? win32 : posix; } - return paths.basename(label); + return pathLib.basename(label); } getWorkspaceLabel(workspace: IWorkspace | IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI, options?: { verbose: boolean }): string { @@ -258,22 +311,26 @@ export class LabelService extends Disposable implements ILabelService { } private doGetSingleFolderWorkspaceLabel(folderUri: URI, options?: { verbose: boolean }): string { - const label = options?.verbose ? this.getUriLabel(folderUri) : basename(folderUri) || '/'; + const label = options?.verbose ? this.getUriLabel(folderUri) : basename(folderUri) || posix.sep; + return this.appendWorkspaceSuffix(label, folderUri); } getSeparator(scheme: string, authority?: string): '/' | '\\' { const formatter = this.findFormatting(URI.from({ scheme, authority })); - return formatter?.separator || '/'; + + return formatter?.separator || posix.sep; } getHostLabel(scheme: string, authority?: string): string { const formatter = this.findFormatting(URI.from({ scheme, authority })); + return formatter?.workspaceSuffix || authority || ''; } getHostTooltip(scheme: string, authority?: string): string | undefined { const formatter = this.findFormatting(URI.from({ scheme, authority })); + return formatter?.workspaceTooltip; } @@ -304,10 +361,10 @@ export class LabelService extends Disposable implements ILabelService { if (query && query[0] === '{' && query[query.length - 1] === '}') { try { return JSON.parse(query)[qsValue] || ''; - } - catch { } + } catch { } } } + return ''; } } @@ -319,11 +376,11 @@ export class LabelService extends Disposable implements ILabelService { } if (formatting.tildify && !forceNoTildify) { - const userHome = this.pathService.resolvedUserHome; - if (userHome) { - label = tildify(label, userHome.fsPath); + if (this.userHome) { + label = tildify(label, this.userHome.fsPath, this.os); } } + if (formatting.authorityPrefix && resource.authority) { label = formatting.authorityPrefix + label; } @@ -331,17 +388,10 @@ export class LabelService extends Disposable implements ILabelService { return label.replace(sepRegexp, formatting.separator); } - private appendSeparatorIfMissing(label: string, formatting: ResourceLabelFormatting): string { - let appendedLabel = label; - if (!label.endsWith(formatting.separator)) { - appendedLabel += formatting.separator; - } - return appendedLabel; - } - private appendWorkspaceSuffix(label: string, uri: URI): string { const formatting = this.findFormatting(uri); const suffix = formatting && (typeof formatting.workspaceSuffix === 'string') ? formatting.workspaceSuffix : undefined; + return suffix ? `${label} [${suffix}]` : 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 9a91b17f7c9..ab94051a7e6 100644 --- a/src/vs/workbench/services/label/test/browser/label.test.ts +++ b/src/vs/workbench/services/label/test/browser/label.test.ts @@ -5,18 +5,19 @@ import * as resources from 'vs/base/common/resources'; import * as assert from 'assert'; -import { TestEnvironmentService, TestPathService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestEnvironmentService, TestPathService, TestRemoteAgentService } from 'vs/workbench/test/browser/workbenchTestServices'; import { URI } from 'vs/base/common/uri'; import { LabelService } from 'vs/workbench/services/label/common/labelService'; import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; import { WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { Workspace } from 'vs/platform/workspace/test/common/testWorkspace'; +import { isWindows } from 'vs/base/common/platform'; suite('URI Label', () => { let labelService: LabelService; setup(() => { - labelService = new LabelService(TestEnvironmentService, new TestContextService(), new TestPathService()); + labelService = new LabelService(TestEnvironmentService, new TestContextService(), new TestPathService(), new TestRemoteAgentService()); }); test('custom scheme', function () { @@ -176,7 +177,9 @@ suite('multi-root workspace', () => { new WorkspaceFolder({ uri: tests, index: 1, name: 'Tests' }), new WorkspaceFolder({ uri: other, index: 2, name: resources.basename(other) }), ])), - new TestPathService()); + new TestPathService(), + new TestRemoteAgentService() + ); }); test('labels of files in multiroot workspaces are the foldername followed by offset from the folder', () => { @@ -249,6 +252,27 @@ suite('multi-root workspace', () => { assert.strictEqual(generated, label, path); }); }); + + test('relative label without formatter', () => { + const rootFolder = URI.parse('myscheme://myauthority/'); + + labelService = new LabelService( + TestEnvironmentService, + new TestContextService( + new Workspace('test-workspace', [ + new WorkspaceFolder({ uri: rootFolder, index: 0, name: 'FSProotFolder' }), + ])), + new TestPathService(undefined, rootFolder.scheme), + new TestRemoteAgentService() + ); + + const generated = labelService.getUriLabel(URI.parse('myscheme://myauthority/some/folder/test.txt'), { relative: true }); + if (isWindows) { + assert.strictEqual(generated, 'some\\folder\\test.txt'); + } else { + assert.strictEqual(generated, 'some/folder/test.txt'); + } + }); }); suite('workspace at FSP root', () => { @@ -263,7 +287,9 @@ suite('workspace at FSP root', () => { new Workspace('test-workspace', [ new WorkspaceFolder({ uri: rootFolder, index: 0, name: 'FSProotFolder' }), ])), - new TestPathService()); + new TestPathService(), + new TestRemoteAgentService() + ); labelService.registerFormatter({ scheme: 'myscheme', formatting: { diff --git a/src/vs/workbench/services/label/test/electron-browser/label.test.ts b/src/vs/workbench/services/label/test/electron-browser/label.test.ts index 24c474bddda..ad2976d6bb7 100644 --- a/src/vs/workbench/services/label/test/electron-browser/label.test.ts +++ b/src/vs/workbench/services/label/test/electron-browser/label.test.ts @@ -11,13 +11,14 @@ import { isWindows } from 'vs/base/common/platform'; import { LabelService } from 'vs/workbench/services/label/common/labelService'; import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; import { TestNativePathService, TestEnvironmentService } from 'vs/workbench/test/electron-browser/workbenchTestServices'; +import { TestRemoteAgentService } from 'vs/workbench/test/browser/workbenchTestServices'; suite('URI Label', () => { let labelService: LabelService; setup(() => { - labelService = new LabelService(TestEnvironmentService, new TestContextService(), new TestNativePathService()); + labelService = new LabelService(TestEnvironmentService, new TestContextService(), new TestNativePathService(), new TestRemoteAgentService()); }); test('file scheme', function () { diff --git a/src/vs/workbench/services/language/common/languageService.ts b/src/vs/workbench/services/language/common/languageService.ts index 90c599c26ed..3b60d31507a 100644 --- a/src/vs/workbench/services/language/common/languageService.ts +++ b/src/vs/workbench/services/language/common/languageService.ts @@ -16,6 +16,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { ExtensionMessageCollector, ExtensionsRegistry, IExtensionPoint, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ILogService } from 'vs/platform/log/common/log'; export interface IRawLanguageExtensionPoint { id: string; @@ -113,7 +114,8 @@ export class WorkbenchLanguageService extends LanguageService { constructor( @IExtensionService extensionService: IExtensionService, @IConfigurationService configurationService: IConfigurationService, - @IEnvironmentService environmentService: IEnvironmentService + @IEnvironmentService environmentService: IEnvironmentService, + @ILogService private readonly logService: ILogService ) { super(environmentService.verbose || environmentService.isExtensionDevelopment || !environmentService.isBuilt); this._configurationService = configurationService; @@ -184,6 +186,12 @@ export class WorkbenchLanguageService extends LanguageService { if (configuration.files?.associations) { Object.keys(configuration.files.associations).forEach(pattern => { const langId = configuration.files.associations[pattern]; + if (typeof langId !== 'string') { + this.logService.warn(`Ingnoing configured 'files.associations' for '${pattern}' because its type is not a string but '${typeof langId}'`); + + return; // https://github.com/microsoft/vscode/issues/147284 + } + const mimeType = this.getMimeType(langId) || `text/x-${langId}`; registerConfiguredLanguageAssociation({ id: langId, mime: mimeType, filepattern: pattern }); diff --git a/src/vs/workbench/services/languageDetection/browser/languageDetectionSimpleWorker.ts b/src/vs/workbench/services/languageDetection/browser/languageDetectionSimpleWorker.ts index 2d9166e9ed5..59d8bf7ef8b 100644 --- a/src/vs/workbench/services/languageDetection/browser/languageDetectionSimpleWorker.ts +++ b/src/vs/workbench/services/languageDetection/browser/languageDetectionSimpleWorker.ts @@ -9,7 +9,7 @@ import { IRequestHandler } from 'vs/base/common/worker/simpleWorker'; import { EditorSimpleWorker } from 'vs/editor/common/services/editorSimpleWorker'; import { IEditorWorkerHost } from 'vs/editor/common/services/editorWorkerHost'; -type RegexpModel = { detect: (inp: string, langBiases: Record<string, number>) => string | undefined }; +type RegexpModel = { detect: (inp: string, langBiases: Record<string, number>, supportedLangs?: string[]) => string | undefined }; /** * Called on the worker side @@ -34,7 +34,9 @@ export class LanguageDetectionSimpleWorker extends EditorSimpleWorker { private _modelOperations: ModelOperations | undefined; private _loadFailed: boolean = false; - public async detectLanguage(uri: string, langBiases: Record<string, number> | undefined, preferHistory: boolean): Promise<string | undefined> { + private modelIdToCoreId = new Map<string, string>(); + + public async detectLanguage(uri: string, langBiases: Record<string, number> | undefined, preferHistory: boolean, supportedLangs?: string[]): Promise<string | undefined> { const languages: string[] = []; const confidences: number[] = []; const stopWatch = new StopWatch(true); @@ -43,8 +45,14 @@ export class LanguageDetectionSimpleWorker extends EditorSimpleWorker { const neuralResolver = async () => { for await (const language of this.detectLanguagesImpl(documentTextSample)) { - languages.push(language.languageId); - confidences.push(language.confidence); + if (!this.modelIdToCoreId.has(language.languageId)) { + this.modelIdToCoreId.set(language.languageId, await this._host.fhr('getLanguageId', [language.languageId])); + } + const coreId = this.modelIdToCoreId.get(language.languageId); + if (coreId && (!supportedLangs?.length || supportedLangs.includes(coreId))) { + languages.push(coreId); + confidences.push(language.confidence); + } } stopWatch.stop(); @@ -55,15 +63,7 @@ export class LanguageDetectionSimpleWorker extends EditorSimpleWorker { return undefined; }; - const historicalResolver = async () => { - if (langBiases) { - const regexpDetection = await this.runRegexpModel(documentTextSample, langBiases); - if (regexpDetection) { - return regexpDetection; - } - } - return undefined; - }; + const historicalResolver = async () => this.runRegexpModel(documentTextSample, langBiases ?? {}, supportedLangs); if (preferHistory) { const history = await historicalResolver(); @@ -112,11 +112,22 @@ export class LanguageDetectionSimpleWorker extends EditorSimpleWorker { } } - private async runRegexpModel(content: string, langBiases: Record<string, number>): Promise<string | undefined> { + private async runRegexpModel(content: string, langBiases: Record<string, number>, supportedLangs?: string[]): Promise<string | undefined> { const regexpModel = await this.getRegexpModel(); if (!regexpModel) { return; } - const detected = regexpModel.detect(content, langBiases); + if (supportedLangs?.length) { + // When using supportedLangs, normally computed biases are too extreme. Just use a "bitmask" of sorts. + for (const lang of Object.keys(langBiases)) { + if (supportedLangs.includes(lang)) { + langBiases[lang] = 1; + } else { + langBiases[lang] = 0; + } + } + } + + const detected = regexpModel.detect(content, langBiases, supportedLangs); return detected; } @@ -156,21 +167,21 @@ export class LanguageDetectionSimpleWorker extends EditorSimpleWorker { // For the following languages, we increase the confidence because // these are commonly used languages in VS Code and supported // by the model. - case 'javascript': + case 'js': case 'html': case 'json': - case 'typescript': + case 'ts': case 'css': - case 'python': + case 'py': case 'xml': case 'php': modelResult.confidence += LanguageDetectionSimpleWorker.positiveConfidenceCorrectionBucket1; break; // case 'yaml': // YAML has been know to cause incorrect language detection because the language is pretty simple. We don't want to increase the confidence for this. case 'cpp': - case 'shellscript': + case 'sh': case 'java': - case 'csharp': + case 'cs': case 'c': modelResult.confidence += LanguageDetectionSimpleWorker.positiveConfidenceCorrectionBucket2; break; diff --git a/src/vs/workbench/services/languageDetection/browser/languageDetectionWorkerServiceImpl.ts b/src/vs/workbench/services/languageDetection/browser/languageDetectionWorkerServiceImpl.ts index eaa283b67d3..d3428409896 100644 --- a/src/vs/workbench/services/languageDetection/browser/languageDetectionWorkerServiceImpl.ts +++ b/src/vs/workbench/services/languageDetection/browser/languageDetectionWorkerServiceImpl.ts @@ -53,7 +53,7 @@ export class LanguageDetectionService extends Disposable implements ILanguageDet constructor( @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, - @ILanguageService private readonly _languageService: ILanguageService, + @ILanguageService languageService: ILanguageService, @IConfigurationService private readonly _configurationService: IConfigurationService, @IDiagnosticsService private readonly _diagnosticsService: IDiagnosticsService, @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, @@ -68,6 +68,7 @@ export class LanguageDetectionService extends Disposable implements ILanguageDet this._languageDetectionWorkerClient = new LanguageDetectionWorkerClient( modelService, + languageService, telemetryService, // TODO: See if it's possible to bundle vscode-languagedetection this._environmentService.isBuilt && !isWeb @@ -95,7 +96,7 @@ export class LanguageDetectionService extends Disposable implements ILanguageDet let count = 0; for (const ext of fileExtensions.extensions) { - const langId = this.getLanguageId(ext); + const langId = this._languageDetectionWorkerClient.getLanguageId(ext); if (langId && count < TOP_LANG_COUNTS) { this.workspaceLanguageIds.add(langId); count++; @@ -109,15 +110,6 @@ export class LanguageDetectionService extends Disposable implements ILanguageDet return !!languageId && this._configurationService.getValue<boolean>(LanguageDetectionService.enablementSettingKey, { overrideIdentifier: languageId }); } - private getLanguageId(language: string | undefined): string | undefined { - if (!language) { - return undefined; - } - if (this._languageService.isRegisteredLanguageId(language)) { - return language; - } - return this._languageService.guessLanguageIdByFilepathOrFirstLine(URI.file(`file.${language}`)) ?? undefined; - } private getLanguageBiases(): Record<string, number> { if (!this.dirtyBiases) { return this.langBiases; } @@ -147,19 +139,14 @@ export class LanguageDetectionService extends Disposable implements ILanguageDet return biases; } - async detectLanguage(resource: URI): Promise<string | undefined> { + async detectLanguage(resource: URI, supportedLangs?: string[]): Promise<string | undefined> { const useHistory = this._configurationService.getValue<string[]>(LanguageDetectionService.historyBasedEnablementConfig); const preferHistory = this._configurationService.getValue<boolean>(LanguageDetectionService.preferHistoryConfig); if (useHistory) { await this.resolveWorkspaceLanguageIds(); } const biases = useHistory ? this.getLanguageBiases() : undefined; - const language = await this._languageDetectionWorkerClient.detectLanguage(resource, biases, preferHistory); - - if (language) { - return this.getLanguageId(language); - } - return undefined; + return this._languageDetectionWorkerClient.detectLanguage(resource, biases, preferHistory, supportedLangs); } private initEditorOpenedListeners(storageService: IStorageService) { @@ -234,6 +221,7 @@ export class LanguageDetectionWorkerClient extends EditorWorkerClient { constructor( modelService: IModelService, + private readonly _languageService: ILanguageService, private readonly _telemetryService: ITelemetryService, private readonly _indexJsUri: string, private readonly _modelJsonUri: string, @@ -260,6 +248,14 @@ export class LanguageDetectionWorkerClient extends EditorWorkerClient { return this.workerPromise; } + private _guessLanguageIdByUri(uri: URI): string | undefined { + const guess = this._languageService.guessLanguageIdByFilepathOrFirstLine(uri); + if (guess && guess !== 'unknown') { + return guess; + } + return undefined; + } + override async _getProxy(): Promise<LanguageDetectionSimpleWorker> { return (await this._getOrCreateLanguageDetectionWorker()).getProxyObject(); } @@ -275,6 +271,8 @@ export class LanguageDetectionWorkerClient extends EditorWorkerClient { return this.getWeightsUri(); case 'getRegexpModelUri': return this.getRegexpModelUri(); + case 'getLanguageId': + return this.getLanguageId(args[0]); case 'sendTelemetryEvent': return this.sendTelemetryEvent(args[0], args[1], args[2]); default: @@ -286,6 +284,20 @@ export class LanguageDetectionWorkerClient extends EditorWorkerClient { return this._indexJsUri; } + getLanguageId(languageIdOrExt: string | undefined) { + if (!languageIdOrExt) { + return undefined; + } + if (this._languageService.isRegisteredLanguageId(languageIdOrExt)) { + return languageIdOrExt; + } + const guessed = this._guessLanguageIdByUri(URI.file(`file.${languageIdOrExt}`)); + if (!guessed || guessed === 'unknown') { + return undefined; + } + return guessed; + } + async getModelJsonUri() { return this._modelJsonUri; } @@ -306,9 +318,35 @@ export class LanguageDetectionWorkerClient extends EditorWorkerClient { }); } - public async detectLanguage(resource: URI, langBiases: Record<string, number> | undefined, preferHistory: boolean): Promise<string | undefined> { + public async detectLanguage(resource: URI, langBiases: Record<string, number> | undefined, preferHistory: boolean, supportedLangs?: string[]): Promise<string | undefined> { + const startTime = Date.now(); + const quickGuess = this._guessLanguageIdByUri(resource); + if (quickGuess) { + return quickGuess; + } + await this._withSyncedResources([resource]); - return (await this._getProxy()).detectLanguage(resource.toString(), langBiases, preferHistory); + const modelId = await (await this._getProxy()).detectLanguage(resource.toString(), langBiases, preferHistory, supportedLangs); + const langaugeId = this.getLanguageId(modelId); + + const LanguageDetectionStatsId = 'automaticlanguagedetection.perf'; + + interface ILanguageDetectionPerf { + timeSpent: number; + detection: string; + } + + type LanguageDetectionPerfClassification = { + timeSpent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; + detection: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; + }; + + this._telemetryService.publicLog2<ILanguageDetectionPerf, LanguageDetectionPerfClassification>(LanguageDetectionStatsId, { + timeSpent: Date.now() - startTime, + detection: langaugeId || 'unknown', + }); + + return langaugeId; } } diff --git a/src/vs/workbench/services/languageDetection/common/languageDetectionWorkerService.ts b/src/vs/workbench/services/languageDetection/common/languageDetectionWorkerService.ts index 2b725466178..0fe24c9c6fd 100644 --- a/src/vs/workbench/services/languageDetection/common/languageDetectionWorkerService.ts +++ b/src/vs/workbench/services/languageDetection/common/languageDetectionWorkerService.ts @@ -19,9 +19,10 @@ export interface ILanguageDetectionService { /** * @param resource The resource to detect the language for. + * @param supportedLangs Optional. When populated, the model will only return languages from the provided list * @returns the language id for the given resource or undefined if the model is not confident enough. */ - detectLanguage(resource: URI): Promise<string | undefined>; + detectLanguage(resource: URI, supportedLangs?: string[]): Promise<string | undefined>; } //#region Telemetry events diff --git a/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts b/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts index 6915f6f035d..96c7fcbff3f 100644 --- a/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts +++ b/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts @@ -168,9 +168,10 @@ export class BrowserLifecycleService extends AbstractLifecycleService { const logService = this.logService; this._onWillShutdown.fire({ reason: ShutdownReason.QUIT, - token: CancellationToken.None, // Unsupported in web - join(promise, id) { - logService.error(`[lifecycle] Long running operations during shutdown are unsupported in the web (id: ${id})`); + joiners: () => [], // Unsupported in web + token: CancellationToken.None, // Unsupported in web + join(promise, joiner) { + logService.error(`[lifecycle] Long running operations during shutdown are unsupported in the web (id: ${joiner.id})`); }, force: () => { /* No-Op in web */ }, }); diff --git a/src/vs/workbench/services/lifecycle/common/lifecycle.ts b/src/vs/workbench/services/lifecycle/common/lifecycle.ts index 199577ebfde..a8c2c9e2cd6 100644 --- a/src/vs/workbench/services/lifecycle/common/lifecycle.ts +++ b/src/vs/workbench/services/lifecycle/common/lifecycle.ts @@ -65,6 +65,11 @@ export interface BeforeShutdownErrorEvent { readonly error: Error; } +export interface IWillShutdownEventJoiner { + id: string; + label: string; +} + /** * An event that is send out when the window closes. Clients have a chance to join the closing * by providing a promise from the join method. Returning a promise is useful in cases of long @@ -90,10 +95,15 @@ export interface WillShutdownEvent { * Allows to join the shutdown. The promise can be a long running operation but it * will block the application from closing. * - * @param id to identify the join operation in case it takes very long or never + * @param joiner to identify the join operation in case it takes very long or never * completes. */ - join(promise: Promise<void>, id: string): void; + join(promise: Promise<void>, joiner: IWillShutdownEventJoiner): void; + + /** + * Allows to access the joiners that have not finished joining this event. + */ + joiners(): IWillShutdownEventJoiner[]; /** * Allows to enforce the shutdown, even when there are @@ -170,7 +180,7 @@ export const enum LifecyclePhase { Eventually = 4 } -export function LifecyclePhaseToString(phase: LifecyclePhase) { +export function LifecyclePhaseToString(phase: LifecyclePhase): string { switch (phase) { case LifecyclePhase.Starting: return 'Starting'; case LifecyclePhase.Ready: return 'Ready'; diff --git a/src/vs/workbench/services/lifecycle/electron-sandbox/lifecycleService.ts b/src/vs/workbench/services/lifecycle/electron-sandbox/lifecycleService.ts index a02dbc84a53..9ad9e92efe9 100644 --- a/src/vs/workbench/services/lifecycle/electron-sandbox/lifecycleService.ts +++ b/src/vs/workbench/services/lifecycle/electron-sandbox/lifecycleService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { handleVetos } from 'vs/platform/lifecycle/common/lifecycle'; -import { ShutdownReason, ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { ShutdownReason, ILifecycleService, IWillShutdownEventJoiner } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals'; import { ILogService } from 'vs/platform/log/common/log'; @@ -155,18 +155,19 @@ export class NativeLifecycleService extends AbstractLifecycleService { protected async handleWillShutdown(reason: ShutdownReason): Promise<void> { const joiners: Promise<void>[] = []; - const pendingJoiners = new Set<string>(); + const pendingJoiners = new Set<IWillShutdownEventJoiner>(); const cts = new CancellationTokenSource(); this._onWillShutdown.fire({ reason, token: cts.token, - join(promise, id) { + joiners: () => Array.from(pendingJoiners.values()), + join(promise, joiner) { joiners.push(promise); // Track promise completion - pendingJoiners.add(id); - promise.finally(() => pendingJoiners.delete(id)); + pendingJoiners.add(joiner); + promise.finally(() => pendingJoiners.delete(joiner)); }, force: () => { cts.dispose(true); @@ -174,7 +175,7 @@ export class NativeLifecycleService extends AbstractLifecycleService { }); const longRunningWillShutdownWarning = disposableTimeout(() => { - this.logService.warn(`[lifecycle] onWillShutdown is taking a long time, pending operations: ${Array.from(pendingJoiners).join(', ')}`); + this.logService.warn(`[lifecycle] onWillShutdown is taking a long time, pending operations: ${Array.from(pendingJoiners).map(joiner => joiner.id).join(', ')}`); }, NativeLifecycleService.WILL_SHUTDOWN_WARNING_DELAY); try { diff --git a/src/vs/workbench/services/lifecycle/test/electron-browser/lifecycleService.test.ts b/src/vs/workbench/services/lifecycle/test/electron-browser/lifecycleService.test.ts index 63cd2106537..4edcf36737f 100644 --- a/src/vs/workbench/services/lifecycle/test/electron-browser/lifecycleService.test.ts +++ b/src/vs/workbench/services/lifecycle/test/electron-browser/lifecycleService.test.ts @@ -132,7 +132,7 @@ suite('Lifecycleservice', function () { joinCalled = true; resolve(); - }), 'test'); + }), { id: 'test', label: 'test' }); }); await lifecycleService.handleWillShutdown(ShutdownReason.QUIT); @@ -148,7 +148,7 @@ suite('Lifecycleservice', function () { joinCalled = true; reject(new Error('Fail')); - }), 'test'); + }), { id: 'test', label: 'test' }); }); await lifecycleService.handleWillShutdown(ShutdownReason.QUIT); diff --git a/src/vs/workbench/services/output/common/output.ts b/src/vs/workbench/services/output/common/output.ts index f12fa430d7a..1d96dcd198a 100644 --- a/src/vs/workbench/services/output/common/output.ts +++ b/src/vs/workbench/services/output/common/output.ts @@ -6,6 +6,149 @@ import { Event, Emitter } from 'vs/base/common/event'; import { Registry } from 'vs/platform/registry/common/platform'; import { URI } from 'vs/base/common/uri'; +import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IFileService, whenProviderRegistered } from 'vs/platform/files/common/files'; +import { ILogService } from 'vs/platform/log/common/log'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { CancellationError, getErrorMessage, isCancellationError } from 'vs/base/common/errors'; +import { CancelablePromise, createCancelablePromise, timeout } from 'vs/base/common/async'; + +/** + * Mime type used by the output editor. + */ +export const OUTPUT_MIME = 'text/x-code-output'; + +/** + * Output resource scheme. + */ +export const OUTPUT_SCHEME = 'output'; + +/** + * Id used by the output editor. + */ +export const OUTPUT_MODE_ID = 'Log'; + +/** + * Mime type used by the log output editor. + */ +export const LOG_MIME = 'text/x-code-log-output'; + +/** + * Log resource scheme. + */ +export const LOG_SCHEME = 'log'; + +/** + * Id used by the log output editor. + */ +export const LOG_MODE_ID = 'log'; + +/** + * Output view id + */ +export const OUTPUT_VIEW_ID = 'workbench.panel.output'; + +export const OUTPUT_SERVICE_ID = 'outputService'; + +export const MAX_OUTPUT_LENGTH = 10000 /* Max. number of output lines to show in output */ * 100 /* Guestimated chars per line */; + +export const CONTEXT_IN_OUTPUT = new RawContextKey<boolean>('inOutput', false); + +export const CONTEXT_ACTIVE_LOG_OUTPUT = new RawContextKey<boolean>('activeLogOutput', false); + +export const CONTEXT_OUTPUT_SCROLL_LOCK = new RawContextKey<boolean>(`outputView.scrollLock`, false); + +export const IOutputService = createDecorator<IOutputService>(OUTPUT_SERVICE_ID); + +/** + * The output service to manage output from the various processes running. + */ +export interface IOutputService { + readonly _serviceBrand: undefined; + + /** + * Given the channel id returns the output channel instance. + * Channel should be first registered via OutputChannelRegistry. + */ + getChannel(id: string): IOutputChannel | undefined; + + /** + * Given the channel id returns the registered output channel descriptor. + */ + getChannelDescriptor(id: string): IOutputChannelDescriptor | undefined; + + /** + * Returns an array of all known output channels descriptors. + */ + getChannelDescriptors(): IOutputChannelDescriptor[]; + + /** + * Returns the currently active channel. + * Only one channel can be active at a given moment. + */ + getActiveChannel(): IOutputChannel | undefined; + + /** + * Show the channel with the passed id. + */ + showChannel(id: string, preserveFocus?: boolean): Promise<void>; + + /** + * Allows to register on active output channel change. + */ + onActiveOutputChannel: Event<string>; +} + +export enum OutputChannelUpdateMode { + Append = 1, + Replace, + Clear +} + +export interface IOutputChannel { + + /** + * Identifier of the output channel. + */ + id: string; + + /** + * Label of the output channel to be displayed to the user. + */ + label: string; + + /** + * URI of the output channel. + */ + uri: URI; + + /** + * Appends output to the channel. + */ + append(output: string): void; + + /** + * Clears all received output for this channel. + */ + clear(): void; + + /** + * Replaces the content of the channel with given output + */ + replace(output: string): void; + + /** + * Update the channel. + */ + update(mode: OutputChannelUpdateMode.Append): void; + update(mode: OutputChannelUpdateMode, till: number): void; + + /** + * Disposes the output channel. + */ + dispose(): void; +} export const Extensions = { OutputChannels: 'workbench.contributions.outputChannels' @@ -82,3 +225,34 @@ class OutputChannelRegistry implements IOutputChannelRegistry { } Registry.add(Extensions.OutputChannels, new OutputChannelRegistry()); + +export function registerLogChannel(id: string, label: string, file: URI, fileService: IFileService, logService: ILogService): CancelablePromise<void> { + return createCancelablePromise(async token => { + await whenProviderRegistered(file, fileService); + const outputChannelRegistry = Registry.as<IOutputChannelRegistry>(Extensions.OutputChannels); + try { + await whenFileExists(file, 1, fileService, logService, token); + outputChannelRegistry.registerChannel({ id, label, file, log: true }); + } catch (error) { + if (!isCancellationError(error)) { + logService.error('Error while registering log channel', file.toString(), getErrorMessage(error)); + } + } + }); +} + +async function whenFileExists(file: URI, trial: number, fileService: IFileService, logService: ILogService, token: CancellationToken): Promise<void> { + const exists = await fileService.exists(file); + if (exists) { + return; + } + if (token.isCancellationRequested) { + throw new CancellationError(); + } + if (trial > 10) { + throw new Error(`Timed out while waiting for file to be created`); + } + logService.debug(`[Registering Log Channel] File does not exist. Waiting for 1s to retry.`, file.toString()); + await timeout(1000, token); + await whenFileExists(file, trial + 1, fileService, logService, token); +} diff --git a/src/vs/workbench/services/path/browser/pathService.ts b/src/vs/workbench/services/path/browser/pathService.ts index 85f31c841fb..215f1ec36ad 100644 --- a/src/vs/workbench/services/path/browser/pathService.ts +++ b/src/vs/workbench/services/path/browser/pathService.ts @@ -9,6 +9,8 @@ import { IPathService, AbstractPathService } from 'vs/workbench/services/path/co import { URI } from 'vs/base/common/uri'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { firstOrDefault } from 'vs/base/common/arrays'; +import { dirname } from 'vs/base/common/resources'; export class BrowserPathService extends AbstractPathService { @@ -17,11 +19,8 @@ export class BrowserPathService extends AbstractPathService { @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, @IWorkspaceContextService contextService: IWorkspaceContextService ) { - super(URI.from({ - scheme: AbstractPathService.findDefaultUriScheme(environmentService, contextService), - authority: environmentService.remoteAuthority, - path: '/' - }), + super( + guessLocalUserHome(environmentService, contextService), remoteAgentService, environmentService, contextService @@ -29,4 +28,33 @@ export class BrowserPathService extends AbstractPathService { } } +function guessLocalUserHome(environmentService: IWorkbenchEnvironmentService, contextService: IWorkspaceContextService): URI { + + // In web we do not really have the concept of a "local" user home + // but we still require it in many places as a fallback. As such, + // we have to come up with a synthetic location derived from the + // environment. + + const workspace = contextService.getWorkspace(); + + const firstFolder = firstOrDefault(workspace.folders); + if (firstFolder) { + return firstFolder.uri; + } + + if (workspace.configuration) { + return dirname(workspace.configuration); + } + + // This is not ideal because with a user home location of `/`, all paths + // will potentially appear with `~/...`, but at this point we really do + // not have any other good alternative. + + return URI.from({ + scheme: AbstractPathService.findDefaultUriScheme(environmentService, contextService), + authority: environmentService.remoteAuthority, + path: '/' + }); +} + registerSingleton(IPathService, BrowserPathService, true); diff --git a/src/vs/workbench/services/path/common/pathService.ts b/src/vs/workbench/services/path/common/pathService.ts index b00c949dc3f..83baa4126b0 100644 --- a/src/vs/workbench/services/path/common/pathService.ts +++ b/src/vs/workbench/services/path/common/pathService.ts @@ -57,6 +57,7 @@ export interface IPathService { * remote's user home directory, otherwise the local one unless * `preferLocal` is set to `true`. */ + userHome(options: { preferLocal: true }): URI; userHome(options?: { preferLocal: boolean }): Promise<URI>; /** @@ -103,7 +104,7 @@ export abstract class AbstractPathService implements IPathService { // User Home this.resolveUserHome = (async () => { const env = await this.remoteAgentService.getEnvironment(); - const userHome = this.maybeUnresolvedUserHome = env?.userHome || localUserHome; + const userHome = this.maybeUnresolvedUserHome = env?.userHome ?? localUserHome; return userHome; })(); @@ -138,7 +139,7 @@ export abstract class AbstractPathService implements IPathService { return AbstractPathService.findDefaultUriScheme(this.environmentService, this.contextService); } - protected static findDefaultUriScheme(environmentService: IWorkbenchEnvironmentService, contextService: IWorkspaceContextService): string { + static findDefaultUriScheme(environmentService: IWorkbenchEnvironmentService, contextService: IWorkspaceContextService): string { if (environmentService.remoteAuthority) { return Schemas.vscodeRemote; } @@ -161,7 +162,9 @@ export abstract class AbstractPathService implements IPathService { return Schemas.file; } - async userHome(options?: { preferLocal: boolean }): Promise<URI> { + userHome(options?: { preferLocal: boolean }): Promise<URI>; + userHome(options: { preferLocal: true }): URI; + userHome(options?: { preferLocal: boolean }): Promise<URI> | URI { return options?.preferLocal ? this.localUserHome : this.resolveUserHome; } diff --git a/src/vs/workbench/services/preferences/browser/preferencesService.ts b/src/vs/workbench/services/preferences/browser/preferencesService.ts index eb5737fa9af..555a3a7ee20 100644 --- a/src/vs/workbench/services/preferences/browser/preferencesService.ts +++ b/src/vs/workbench/services/preferences/browser/preferencesService.ts @@ -17,7 +17,6 @@ import { IModelService } from 'vs/editor/common/services/model'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import * as nls from 'vs/nls'; -import { ICommandService } from 'vs/platform/commands/common/commands'; import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { Extensions, getDefaultValue, IConfigurationRegistry, OVERRIDE_PROPERTY_REGEX } from 'vs/platform/configuration/common/configurationRegistry'; import { EditorResolution } from 'vs/platform/editor/common/editor'; @@ -46,6 +45,7 @@ import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteA import { ITextEditorService } from 'vs/workbench/services/textfile/common/textEditorService'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { isArray, isObject } from 'vs/base/common/types'; +import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController'; const emptyEditableSettingsContent = '{\n}'; @@ -76,7 +76,6 @@ export class PreferencesService extends Disposable implements IPreferencesServic @ILanguageService private readonly languageService: ILanguageService, @ILabelService private readonly labelService: ILabelService, @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService, - @ICommandService private readonly commandService: ICommandService, @ITextEditorService private readonly textEditorService: ITextEditorService ) { super(); @@ -542,7 +541,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic codeEditor.revealPositionNearTop(position); codeEditor.focus(); if (edit) { - await this.commandService.executeCommand('editor.action.triggerSuggest'); + SuggestController.get(codeEditor)?.triggerSuggest(); } } } diff --git a/src/vs/workbench/services/preferences/common/preferencesModels.ts b/src/vs/workbench/services/preferences/common/preferencesModels.ts index df0250c0d61..000a74b77ca 100644 --- a/src/vs/workbench/services/preferences/common/preferencesModels.ts +++ b/src/vs/workbench/services/preferences/common/preferencesModels.ts @@ -903,7 +903,7 @@ export class DefaultSettingsEditorModel extends AbstractSettingsModel implements // Force tokenization now - otherwise it may be slightly delayed, causing a flash of white text const tokenizeTo = Math.min(startLine + 60, this._model.getLineCount()); - this._model.forceTokenization(tokenizeTo); + this._model.tokenization.forceTokenization(tokenizeTo); return { matches, settingsGroups }; } diff --git a/src/vs/workbench/services/preferences/test/browser/preferencesService.test.ts b/src/vs/workbench/services/preferences/test/browser/preferencesService.test.ts index 4f5a7658424..b83421feb0b 100644 --- a/src/vs/workbench/services/preferences/test/browser/preferencesService.test.ts +++ b/src/vs/workbench/services/preferences/test/browser/preferencesService.test.ts @@ -15,8 +15,7 @@ import { TestJSONEditingService } from 'vs/workbench/services/configuration/test import { PreferencesService } from 'vs/workbench/services/preferences/browser/preferencesService'; import { IPreferencesService, ISettingsEditorOptions } from 'vs/workbench/services/preferences/common/preferences'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; -import { TestRemoteAgentService } from 'vs/workbench/services/remote/test/common/testServices'; -import { ITestInstantiationService, TestEditorService, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestRemoteAgentService, ITestInstantiationService, TestEditorService, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; suite('PreferencesService', () => { diff --git a/src/vs/workbench/services/profiles/common/extensionsProfile.ts b/src/vs/workbench/services/profiles/common/extensionsProfile.ts new file mode 100644 index 00000000000..3e10dd0f9f5 --- /dev/null +++ b/src/vs/workbench/services/profiles/common/extensionsProfile.ts @@ -0,0 +1,102 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from 'vs/base/common/cancellation'; +import { IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementService, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { ExtensionType } from 'vs/platform/extensions/common/extensions'; +import { ILogService } from 'vs/platform/log/common/log'; +import { EnablementState, IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { IResourceProfile } from 'vs/workbench/services/profiles/common/profile'; + +interface IProfileExtension { + identifier: IExtensionIdentifier; + preRelease?: boolean; + disabled?: boolean; +} + +export class ExtensionsProfile implements IResourceProfile { + + constructor( + @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, + @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, + @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, + @ILogService private readonly logService: ILogService, + ) { + } + + async getProfileContent(): Promise<string> { + const extensions = await this.getLocalExtensions(); + return JSON.stringify(extensions); + } + + async applyProfile(content: string): Promise<void> { + const profileExtensions: IProfileExtension[] = JSON.parse(content); + const installedExtensions = await this.extensionManagementService.getInstalled(); + const extensionsToEnableOrDisable: { extension: ILocalExtension; enablementState: EnablementState }[] = []; + const extensionsToInstall: IProfileExtension[] = []; + for (const e of profileExtensions) { + const installedExtension = installedExtensions.find(installed => areSameExtensions(installed.identifier, e.identifier)); + if (!installedExtension || installedExtension.preRelease !== e.preRelease) { + extensionsToInstall.push(e); + } + if (installedExtension && this.extensionEnablementService.isEnabled(installedExtension) !== !e.disabled) { + extensionsToEnableOrDisable.push({ extension: installedExtension, enablementState: e.disabled ? EnablementState.DisabledGlobally : EnablementState.EnabledGlobally }); + } + } + const extensionsToUninstall: ILocalExtension[] = installedExtensions.filter(extension => extension.type === ExtensionType.User && !profileExtensions.some(({ identifier }) => areSameExtensions(identifier, extension.identifier))); + for (const { extension, enablementState } of extensionsToEnableOrDisable) { + this.logService.trace(`Profile: Updating extension enablement...`, extension.identifier.id); + await this.extensionEnablementService.setEnablement([extension], enablementState); + this.logService.info(`Profile: Updated extension enablement`, extension.identifier.id); + } + if (extensionsToInstall.length) { + const galleryExtensions = await this.extensionGalleryService.getExtensions(extensionsToInstall.map(e => ({ ...e.identifier, hasPreRelease: e.preRelease })), CancellationToken.None); + await Promise.all(extensionsToInstall.map(async e => { + const extension = galleryExtensions.find(galleryExtension => areSameExtensions(galleryExtension.identifier, e.identifier)); + if (!extension) { + return; + } + if (await this.extensionManagementService.canInstall(extension)) { + this.logService.trace(`Profile: Installing extension...`, e.identifier.id, extension.version); + await this.extensionManagementService.installFromGallery(extension, { isMachineScoped: false, donotIncludePackAndDependencies: true, installPreReleaseVersion: e.preRelease } /* set isMachineScoped value to prevent install and sync dialog in web */); + this.logService.info(`Profile: Installed extension.`, e.identifier.id, extension.version); + } else { + this.logService.info(`Profile: Skipped installing extension because it cannot be installed.`, extension.displayName || extension.identifier.id); + } + })); + } + if (extensionsToUninstall.length) { + await Promise.all(extensionsToUninstall.map(e => this.extensionManagementService.uninstall(e))); + } + } + + private async getLocalExtensions(): Promise<IProfileExtension[]> { + const result: IProfileExtension[] = []; + const installedExtensions = await this.extensionManagementService.getInstalled(undefined); + for (const extension of installedExtensions) { + const { identifier, preRelease } = extension; + const enablementState = this.extensionEnablementService.getEnablementState(extension); + const disabled = !this.extensionEnablementService.isEnabledEnablementState(enablementState); + if (!disabled && extension.type === ExtensionType.System) { + // skip enabled system extensions + continue; + } + if (disabled && enablementState !== EnablementState.DisabledGlobally && enablementState !== EnablementState.DisabledWorkspace) { + //skip extensions that are not disabled by user + continue; + } + const profileExtension: IProfileExtension = { identifier }; + if (disabled) { + profileExtension.disabled = true; + } + if (preRelease) { + profileExtension.preRelease = true; + } + result.push(profileExtension); + } + return result; + } +} diff --git a/src/vs/workbench/services/profiles/common/globalStateProfile.ts b/src/vs/workbench/services/profiles/common/globalStateProfile.ts new file mode 100644 index 00000000000..7016a4c6136 --- /dev/null +++ b/src/vs/workbench/services/profiles/common/globalStateProfile.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IStringDictionary } from 'vs/base/common/collections'; +import { ILogService } from 'vs/platform/log/common/log'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { IResourceProfile } from 'vs/workbench/services/profiles/common/profile'; +import { Extensions, IProfileStorageRegistry } from 'vs/workbench/services/profiles/common/profileStorageRegistry'; + +interface IGlobalState { + storage: IStringDictionary<string>; +} + +export class GlobalStateProfile implements IResourceProfile { + + constructor( + @IStorageService private readonly storageService: IStorageService, + @ILogService private readonly logService: ILogService, + ) { + } + + async getProfileContent(): Promise<string> { + const globalState = await this.getLocalGlobalState(); + return JSON.stringify(globalState); + } + + async applyProfile(content: string): Promise<void> { + const globalState: IGlobalState = JSON.parse(content); + await this.writeLocalGlobalState(globalState); + } + + private async getLocalGlobalState(): Promise<IGlobalState> { + const storage: IStringDictionary<string> = {}; + for (const { key } of Registry.as<IProfileStorageRegistry>(Extensions.ProfileStorageRegistry).all) { + const value = this.storageService.get(key, StorageScope.GLOBAL); + if (value) { + storage[key] = value; + } + } + return { storage }; + } + + private async writeLocalGlobalState(globalState: IGlobalState): Promise<void> { + const profileKeys: string[] = Object.keys(globalState.storage); + const updatedStorage: IStringDictionary<any> = globalState.storage; + for (const { key } of Registry.as<IProfileStorageRegistry>(Extensions.ProfileStorageRegistry).all) { + if (!profileKeys.includes(key)) { + // Remove the key if it does not exist in the profile + updatedStorage[key] = undefined; + } + } + const updatedStorageKeys: string[] = Object.keys(updatedStorage); + if (updatedStorageKeys.length) { + this.logService.trace(`Profile: Updating global state...`); + for (const key of updatedStorageKeys) { + this.storageService.store(key, globalState.storage[key], StorageScope.GLOBAL, StorageTarget.USER); + } + this.logService.info(`Profile: Updated global state`, updatedStorageKeys); + } + } +} diff --git a/src/vs/workbench/services/profiles/common/profile.ts b/src/vs/workbench/services/profiles/common/profile.ts new file mode 100644 index 00000000000..a57331a9c56 --- /dev/null +++ b/src/vs/workbench/services/profiles/common/profile.ts @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { isUndefined } from 'vs/base/common/types'; +import { localize } from 'vs/nls'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; + +export interface IProfile { + readonly name?: string; + readonly settings?: string; + readonly globalState?: string; + readonly extensions?: string; +} + +export function isProfile(thing: any): thing is IProfile { + const candidate = thing as IProfile | undefined; + + return !!(candidate && typeof candidate === 'object' + && (isUndefined(candidate.name) || typeof candidate.name === 'string') + && (isUndefined(candidate.settings) || typeof candidate.settings === 'string') + && (isUndefined(candidate.globalState) || typeof candidate.globalState === 'string') + && (isUndefined(candidate.extensions) || typeof candidate.extensions === 'string')); +} + +export type ProfileCreationOptions = { readonly skipComments: boolean }; + +export const IWorkbenchProfileService = createDecorator<IWorkbenchProfileService>('IWorkbenchProfileService'); +export interface IWorkbenchProfileService { + readonly _serviceBrand: undefined; + + createProfile(options?: ProfileCreationOptions): Promise<IProfile>; + setProfile(profile: IProfile): Promise<void>; +} + +export interface IResourceProfile { + getProfileContent(): Promise<string>; + applyProfile(content: string): Promise<void>; +} + +export const PROFILES_CATEGORY = localize('settings profiles', "Settings Profile"); +export const PROFILE_EXTENSION = 'code-profile'; +export const PROFILE_FILTER = [{ name: localize('profile', "Settings Profile"), extensions: [PROFILE_EXTENSION] }]; diff --git a/src/vs/workbench/services/profiles/common/profileService.ts b/src/vs/workbench/services/profiles/common/profileService.ts new file mode 100644 index 00000000000..053b9e59bcf --- /dev/null +++ b/src/vs/workbench/services/profiles/common/profileService.ts @@ -0,0 +1,65 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; +import { ExtensionsProfile } from 'vs/workbench/services/profiles/common/extensionsProfile'; +import { GlobalStateProfile } from 'vs/workbench/services/profiles/common/globalStateProfile'; +import { IProfile, IWorkbenchProfileService, PROFILES_CATEGORY } from 'vs/workbench/services/profiles/common/profile'; +import { SettingsProfile } from 'vs/workbench/services/profiles/common/settingsProfile'; + +export class WorkbenchProfileService implements IWorkbenchProfileService { + + readonly _serviceBrand: undefined; + + private readonly settingsProfile: SettingsProfile; + private readonly globalStateProfile: GlobalStateProfile; + private readonly extensionsProfile: ExtensionsProfile; + + constructor( + @IInstantiationService instantiationService: IInstantiationService, + @IProgressService private readonly progressService: IProgressService, + @INotificationService private readonly notificationService: INotificationService + ) { + this.settingsProfile = instantiationService.createInstance(SettingsProfile); + this.globalStateProfile = instantiationService.createInstance(GlobalStateProfile); + this.extensionsProfile = instantiationService.createInstance(ExtensionsProfile); + } + + async createProfile(options?: { skipComments: boolean }): Promise<IProfile> { + const settings = await this.settingsProfile.getProfileContent(options); + const globalState = await this.globalStateProfile.getProfileContent(); + const extensions = await this.extensionsProfile.getProfileContent(); + return { + settings, + globalState, + extensions + }; + } + + async setProfile(profile: IProfile): Promise<void> { + await this.progressService.withProgress({ + location: ProgressLocation.Notification, + title: localize('profiles.applying', "{0}: Applying...", PROFILES_CATEGORY), + }, async progress => { + if (profile.settings) { + await this.settingsProfile.applyProfile(profile.settings); + } + if (profile.globalState) { + await this.globalStateProfile.applyProfile(profile.globalState); + } + if (profile.extensions) { + await this.extensionsProfile.applyProfile(profile.extensions); + } + }); + this.notificationService.info(localize('applied profile', "{0}: Applied successfully.", PROFILES_CATEGORY)); + } + +} + +registerSingleton(IWorkbenchProfileService, WorkbenchProfileService); diff --git a/src/vs/workbench/services/profiles/common/profileStorageRegistry.ts b/src/vs/workbench/services/profiles/common/profileStorageRegistry.ts new file mode 100644 index 00000000000..807b20e259b --- /dev/null +++ b/src/vs/workbench/services/profiles/common/profileStorageRegistry.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event, Emitter } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Registry } from 'vs/platform/registry/common/platform'; + +export namespace Extensions { + export const ProfileStorageRegistry = 'workbench.registry.profile.storage'; +} + +export interface IProfileStorageKey { + readonly key: string; + readonly description?: string; +} + +/** + * A registry for storage keys used for profiles + */ +export interface IProfileStorageRegistry { + /** + * An event that is triggered when storage keys are registered. + */ + readonly onDidRegister: Event<readonly IProfileStorageKey[]>; + + /** + * All registered storage keys + */ + readonly all: IProfileStorageKey[]; + + /** + * Register profile storage keys + * + * @param keys keys to register + */ + registerKeys(keys: IProfileStorageKey[]): void; +} + +class ProfileStorageRegistryImpl extends Disposable implements IProfileStorageRegistry { + + private readonly _onDidRegister = this._register(new Emitter<readonly IProfileStorageKey[]>()); + readonly onDidRegister = this._onDidRegister.event; + + private readonly storageKeys = new Map<string, IProfileStorageKey>(); + + get all(): IProfileStorageKey[] { + return [...this.storageKeys.values()].flat(); + } + + registerKeys(keys: IProfileStorageKey[]): void { + for (const key of keys) { + this.storageKeys.set(key.key, key); + } + this._onDidRegister.fire(keys); + } + +} + +Registry.add(Extensions.ProfileStorageRegistry, new ProfileStorageRegistryImpl()); + diff --git a/src/vs/workbench/services/profiles/common/settingsProfile.ts b/src/vs/workbench/services/profiles/common/settingsProfile.ts new file mode 100644 index 00000000000..906e0891449 --- /dev/null +++ b/src/vs/workbench/services/profiles/common/settingsProfile.ts @@ -0,0 +1,69 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { VSBuffer } from 'vs/base/common/buffer'; +import { ConfigurationScope, Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IFileService } from 'vs/platform/files/common/files'; +import { ILogService } from 'vs/platform/log/common/log'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { removeComments, updateIgnoredSettings } from 'vs/platform/userDataSync/common/settingsMerge'; +import { IUserDataSyncUtilService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IResourceProfile, ProfileCreationOptions } from 'vs/workbench/services/profiles/common/profile'; + +interface ISettingsContent { + settings: string; +} + +export class SettingsProfile implements IResourceProfile { + + constructor( + @IFileService private readonly fileService: IFileService, + @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IUserDataSyncUtilService private readonly userDataSyncUtilService: IUserDataSyncUtilService, + @ILogService private readonly logService: ILogService, + ) { + } + + async getProfileContent(options?: ProfileCreationOptions): Promise<string> { + const ignoredSettings = this.getIgnoredSettings(); + const formattingOptions = await this.userDataSyncUtilService.resolveFormattingOptions(this.environmentService.settingsResource); + const localContent = await this.getLocalFileContent(); + let settingsProfileContent = updateIgnoredSettings(localContent || '{}', '{}', ignoredSettings, formattingOptions); + if (options?.skipComments) { + settingsProfileContent = removeComments(settingsProfileContent, formattingOptions); + } + const settingsContent: ISettingsContent = { + settings: settingsProfileContent + }; + return JSON.stringify(settingsContent); + } + + async applyProfile(content: string): Promise<void> { + const settingsContent: ISettingsContent = JSON.parse(content); + this.logService.trace(`Profile: Applying settings...`); + const localSettingsContent = await this.getLocalFileContent(); + const formattingOptions = await this.userDataSyncUtilService.resolveFormattingOptions(this.environmentService.settingsResource); + const contentToUpdate = updateIgnoredSettings(settingsContent.settings, localSettingsContent || '{}', this.getIgnoredSettings(), formattingOptions); + await this.fileService.writeFile(this.environmentService.settingsResource, VSBuffer.fromString(contentToUpdate)); + this.logService.info(`Profile: Applied settings`); + } + + private getIgnoredSettings(): string[] { + const allSettings = Registry.as<IConfigurationRegistry>(Extensions.Configuration).getConfigurationProperties(); + const ignoredSettings = Object.keys(allSettings).filter(key => allSettings[key]?.scope === ConfigurationScope.MACHINE || allSettings[key]?.scope === ConfigurationScope.MACHINE_OVERRIDABLE); + return ignoredSettings; + } + + private async getLocalFileContent(): Promise<string | null> { + try { + const content = await this.fileService.readFile(this.environmentService.settingsResource); + return content.value.toString(); + } catch (error) { + return null; + } + } + +} diff --git a/src/vs/workbench/services/remote/test/common/testServices.ts b/src/vs/workbench/services/remote/test/common/testServices.ts deleted file mode 100644 index f4323c7bc65..00000000000 --- a/src/vs/workbench/services/remote/test/common/testServices.ts +++ /dev/null @@ -1,53 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 { IDiagnosticInfoOptions, IDiagnosticInfo } from 'vs/platform/diagnostics/common/diagnostics'; -import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { ISocketFactory } from 'vs/platform/remote/common/remoteAgentConnection'; -import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment'; -import { ITelemetryData, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; -import { IExtensionHostExitInfo, IRemoteAgentConnection, IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; - -export class TestRemoteAgentService implements IRemoteAgentService { - _serviceBrand: undefined; - socketFactory: ISocketFactory = { - connect() { } - }; - getConnection(): IRemoteAgentConnection | null { - throw new Error('Method not implemented.'); - } - getEnvironment(): Promise<IRemoteAgentEnvironment | null> { - throw new Error('Method not implemented.'); - } - getRawEnvironment(): Promise<IRemoteAgentEnvironment | null> { - throw new Error('Method not implemented.'); - } - getExtensionHostExitInfo(reconnectionToken: string): Promise<IExtensionHostExitInfo | null> { - throw new Error('Method not implemented.'); - } - whenExtensionsReady(): Promise<void> { - throw new Error('Method not implemented.'); - } - scanExtensions(skipExtensions?: ExtensionIdentifier[]): Promise<IExtensionDescription[]> { - throw new Error('Method not implemented.'); - } - scanSingleExtension(extensionLocation: URI, isBuiltin: boolean): Promise<IExtensionDescription | null> { - throw new Error('Method not implemented.'); - } - getDiagnosticInfo(options: IDiagnosticInfoOptions): Promise<IDiagnosticInfo | undefined> { - throw new Error('Method not implemented.'); - } - updateTelemetryLevel(telemetryLevel: TelemetryLevel): Promise<void> { - throw new Error('Method not implemented.'); - } - logTelemetry(eventName: string, data?: ITelemetryData): Promise<void> { - throw new Error('Method not implemented.'); - } - flushTelemetry(): Promise<void> { - throw new Error('Method not implemented.'); - } - -} diff --git a/src/vs/workbench/services/search/common/fileSearchManager.ts b/src/vs/workbench/services/search/common/fileSearchManager.ts index 27453fd8138..c5c8b2d10b4 100644 --- a/src/vs/workbench/services/search/common/fileSearchManager.ts +++ b/src/vs/workbench/services/search/common/fileSearchManager.ts @@ -224,8 +224,8 @@ class FileSearchEngine { // Check exclude pattern // If the user searches for the exact file name, we adjust the glob matching - // to ignore filtering by siblings because the user seems to know what she - // is searching for and we want to include the result in that case anyway + // to ignore filtering by siblings because the user seems to know what they + // are searching for and we want to include the result in that case anyway if (queryTester.matchesExcludesSync(relativePath, basename, filePattern !== basename ? hasSibling : undefined)) { continue; } diff --git a/src/vs/workbench/services/search/common/searchExtTypes.ts b/src/vs/workbench/services/search/common/searchExtTypes.ts index 27f646183f4..5b8449ffd83 100644 --- a/src/vs/workbench/services/search/common/searchExtTypes.ts +++ b/src/vs/workbench/services/search/common/searchExtTypes.ts @@ -74,7 +74,7 @@ export interface RelativePattern { * (like `** /*.{ts,js}` without space before / or `*.{ts,js}`) or a [relative pattern](#RelativePattern). * * Glob patterns can have the following syntax: - * * `*` to match one or more characters in a path segment + * * `*` to match zero or more characters in a path segment * * `?` to match on one character in a path segment * * `**` to match any number of path segments, including none * * `{}` to group conditions (e.g. `** /*.{ts,js}` without space before / matches all TypeScript and JavaScript files) diff --git a/src/vs/workbench/services/search/node/fileSearch.ts b/src/vs/workbench/services/search/node/fileSearch.ts index 60719151216..e6b9bb1633c 100644 --- a/src/vs/workbench/services/search/node/fileSearch.ts +++ b/src/vs/workbench/services/search/node/fileSearch.ts @@ -429,8 +429,8 @@ export class FileWalker { // Check exclude pattern // If the user searches for the exact file name, we adjust the glob matching - // to ignore filtering by siblings because the user seems to know what she - // is searching for and we want to include the result in that case anyway + // to ignore filtering by siblings because the user seems to know what they + // are searching for and we want to include the result in that case anyway if (excludePattern.test(relativePath, basename, filePattern !== basename ? hasSibling : undefined)) { continue; } @@ -479,8 +479,8 @@ export class FileWalker { // Check exclude pattern // If the user searches for the exact file name, we adjust the glob matching - // to ignore filtering by siblings because the user seems to know what she - // is searching for and we want to include the result in that case anyway + // to ignore filtering by siblings because the user seems to know what they + // are searching for and we want to include the result in that case anyway const currentRelativePath = relativeParentPath ? [relativeParentPath, file].join(path.sep) : file; if (this.folderExcludePatterns.get(folderQuery.folder.fsPath)!.test(currentRelativePath, file, this.config.filePattern !== file ? hasSibling : undefined)) { return clb(null); diff --git a/src/vs/workbench/services/search/worker/localFileSearch.ts b/src/vs/workbench/services/search/worker/localFileSearch.ts index e083611508a..108ffef411e 100644 --- a/src/vs/workbench/services/search/worker/localFileSearch.ts +++ b/src/vs/workbench/services/search/worker/localFileSearch.ts @@ -190,7 +190,7 @@ export class LocalFileSearchSimpleWorker implements ILocalFileSearchSimpleWorker const isFileIncluded = (path: string, basename: string, hasSibling: (query: string) => boolean) => { path = path.slice(1); if (folderExcludes(path, basename, hasSibling)) { return false; } - if (!pathIncludedInQuery(queryProps, URI.file(path), extUri)) { return false; } + if (!pathIncludedInQuery(queryProps, path, extUri)) { return false; } return true; }; @@ -325,13 +325,13 @@ function pathExcludedInQuery(queryProps: ICommonQueryProps<URI>, fsPath: string) return false; } -function pathIncludedInQuery(queryProps: ICommonQueryProps<URI>, path: URI, extUri: ExtUri): boolean { - if (queryProps.excludePattern && glob.match(queryProps.excludePattern, path.fsPath)) { +function pathIncludedInQuery(queryProps: ICommonQueryProps<URI>, path: string, extUri: ExtUri): boolean { + if (queryProps.excludePattern && glob.match(queryProps.excludePattern, path)) { return false; } if (queryProps.includePattern || queryProps.usingSearchPaths) { - if (queryProps.includePattern && glob.match(queryProps.includePattern, path.fsPath)) { + if (queryProps.includePattern && glob.match(queryProps.includePattern, path)) { return true; } @@ -340,8 +340,9 @@ function pathIncludedInQuery(queryProps: ICommonQueryProps<URI>, path: URI, extU return !!queryProps.folderQueries && queryProps.folderQueries.some(fq => { const searchPath = fq.folder; - if (extUri.isEqualOrParent(path, searchPath)) { - const relPath = paths.relative(searchPath.path, path.path); + const uri = URI.file(path); + if (extUri.isEqualOrParent(uri, searchPath)) { + const relPath = paths.relative(searchPath.path, uri.path); return !fq.includePattern || !!glob.match(fq.includePattern, relPath); } else { return false; diff --git a/src/vs/workbench/services/sharedProcess/electron-sandbox/sharedProcessService.ts b/src/vs/workbench/services/sharedProcess/electron-sandbox/sharedProcessService.ts index 2806378bc82..6590bc91938 100644 --- a/src/vs/workbench/services/sharedProcess/electron-sandbox/sharedProcessService.ts +++ b/src/vs/workbench/services/sharedProcess/electron-sandbox/sharedProcessService.ts @@ -42,6 +42,7 @@ export class SharedProcessService extends Disposable implements ISharedProcessSe // Acquire a message port connected to the shared process mark('code/willConnectSharedProcess'); + this.logService.trace('Renderer->SharedProcess#connect: before acquirePort'); const port = await acquirePort('vscode:createSharedProcessMessageChannel', 'vscode:createSharedProcessMessageChannelResult'); mark('code/didConnectSharedProcess'); this.logService.trace('Renderer->SharedProcess#connect: connection established'); diff --git a/src/vs/workbench/services/telemetry/browser/telemetryService.ts b/src/vs/workbench/services/telemetry/browser/telemetryService.ts index c65ee021e76..4b4d5ead47c 100644 --- a/src/vs/workbench/services/telemetry/browser/telemetryService.ts +++ b/src/vs/workbench/services/telemetry/browser/telemetryService.ts @@ -5,6 +5,7 @@ import type { ApplicationInsights } from '@microsoft/applicationinsights-web'; import { Disposable } from 'vs/base/common/lifecycle'; +import { IObservableValue } from 'vs/base/common/observableValue'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ILoggerService } from 'vs/platform/log/common/log'; @@ -25,7 +26,7 @@ class WebAppInsightsAppender implements ITelemetryAppender { private _telemetryCache: { eventName: string; data: any }[] = []; constructor(private _eventPrefix: string, aiKey: string) { - const endpointUrl = 'https://vortex.data.microsoft.com/collect/v1'; + const endpointUrl = 'https://mobile.events.data.microsoft.com/collect/v1'; import('@microsoft/applicationinsights-web').then(aiLibrary => { this._aiClient = new aiLibrary.ApplicationInsights({ config: { @@ -138,7 +139,7 @@ export class TelemetryService extends Disposable implements ITelemetryService { return this.impl.setExperimentProperty(name, value); } - get telemetryLevel(): TelemetryLevel { + get telemetryLevel(): IObservableValue<TelemetryLevel> { return this.impl.telemetryLevel; } diff --git a/src/vs/workbench/services/telemetry/electron-sandbox/telemetryService.ts b/src/vs/workbench/services/telemetry/electron-sandbox/telemetryService.ts index ca23c70a78f..a91c4a23be9 100644 --- a/src/vs/workbench/services/telemetry/electron-sandbox/telemetryService.ts +++ b/src/vs/workbench/services/telemetry/electron-sandbox/telemetryService.ts @@ -17,6 +17,7 @@ import { TelemetryService as BaseTelemetryService, ITelemetryServiceConfig } fro import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ClassifiedEvent, StrictPropertyCheck, GDPRClassification } from 'vs/platform/telemetry/common/gdprTypings'; import { IFileService } from 'vs/platform/files/common/files'; +import { IObservableValue } from 'vs/base/common/observableValue'; export class TelemetryService extends Disposable implements ITelemetryService { @@ -56,7 +57,7 @@ export class TelemetryService extends Disposable implements ITelemetryService { return this.impl.setExperimentProperty(name, value); } - get telemetryLevel(): TelemetryLevel { + get telemetryLevel(): IObservableValue<TelemetryLevel> { return this.impl.telemetryLevel; } diff --git a/src/vs/workbench/services/textMate/browser/abstractTextMateService.ts b/src/vs/workbench/services/textMate/browser/abstractTextMateService.ts index 57aa1cd2c21..0df6157e1bd 100644 --- a/src/vs/workbench/services/textMate/browser/abstractTextMateService.ts +++ b/src/vs/workbench/services/textMate/browser/abstractTextMateService.ts @@ -133,6 +133,16 @@ export abstract class AbstractTextMateService extends Disposable implements ITex validLanguageId = grammar.language; } + function asStringArray(array: unknown, defaultValue: string[]): string[] { + if (!Array.isArray(array)) { + return defaultValue; + } + if (!array.every(e => typeof e === 'string')) { + return defaultValue; + } + return array; + } + this._grammarDefinitions.push({ location: grammarLocation, language: validLanguageId ? validLanguageId : undefined, @@ -140,6 +150,8 @@ export abstract class AbstractTextMateService extends Disposable implements ITex embeddedLanguages: embeddedLanguages, tokenTypes: tokenTypes, injectTo: grammar.injectTo, + balancedBracketSelectors: asStringArray(grammar.balancedBracketScopes, ['*']), + unbalancedBracketSelectors: asStringArray(grammar.unbalancedBracketScopes, []), }); if (validLanguageId) { diff --git a/src/vs/workbench/services/textMate/browser/nativeTextMateService.ts b/src/vs/workbench/services/textMate/browser/nativeTextMateService.ts index de5b056af74..bbf4fddc3e0 100644 --- a/src/vs/workbench/services/textMate/browser/nativeTextMateService.ts +++ b/src/vs/workbench/services/textMate/browser/nativeTextMateService.ts @@ -120,7 +120,7 @@ class ModelWorkerTextMateTokenizer extends Disposable { } } - this._model.setTokens(tokens); + this._model.tokenization.setTokens(tokens); } } diff --git a/src/vs/workbench/services/textMate/browser/textMateWorker.ts b/src/vs/workbench/services/textMate/browser/textMateWorker.ts index d5169697d97..6b848d33c24 100644 --- a/src/vs/workbench/services/textMate/browser/textMateWorker.ts +++ b/src/vs/workbench/services/textMate/browser/textMateWorker.ts @@ -25,6 +25,8 @@ export interface IValidGrammarDefinitionDTO { embeddedLanguages: IValidEmbeddedLanguagesMap; tokenTypes: IValidTokenTypeMap; injectTo?: string[]; + balancedBracketSelectors: string[]; + unbalancedBracketSelectors: string[]; } export interface ICreateData { @@ -143,6 +145,8 @@ export class TextMateWorker { embeddedLanguages: def.embeddedLanguages, tokenTypes: def.tokenTypes, injectTo: def.injectTo, + balancedBracketSelectors: def.balancedBracketSelectors, + unbalancedBracketSelectors: def.unbalancedBracketSelectors, }; }); this._grammarFactory = this._loadTMGrammarFactory(grammarDefinitions); diff --git a/src/vs/workbench/services/textMate/common/TMGrammarFactory.ts b/src/vs/workbench/services/textMate/common/TMGrammarFactory.ts index 2bb9be17ad0..fc43c1b81da 100644 --- a/src/vs/workbench/services/textMate/common/TMGrammarFactory.ts +++ b/src/vs/workbench/services/textMate/common/TMGrammarFactory.ts @@ -138,7 +138,16 @@ export class TMGrammarFactory extends Disposable { let grammar: IGrammar | null; try { - grammar = await this._grammarRegistry.loadGrammarWithConfiguration(scopeName, encodedLanguageId, { embeddedLanguages, tokenTypes: <any>grammarDefinition.tokenTypes }); + grammar = await this._grammarRegistry.loadGrammarWithConfiguration( + scopeName, + encodedLanguageId, + { + embeddedLanguages, + tokenTypes: <any>grammarDefinition.tokenTypes, + balancedBracketSelectors: grammarDefinition.balancedBracketSelectors, + unbalancedBracketSelectors: grammarDefinition.unbalancedBracketSelectors, + } + ); } catch (err) { if (err.message && err.message.startsWith('No grammar provided for')) { // No TM grammar defined diff --git a/src/vs/workbench/services/textMate/common/TMGrammars.ts b/src/vs/workbench/services/textMate/common/TMGrammars.ts index e67da586b66..b460653c97b 100644 --- a/src/vs/workbench/services/textMate/common/TMGrammars.ts +++ b/src/vs/workbench/services/textMate/common/TMGrammars.ts @@ -22,6 +22,8 @@ export interface ITMSyntaxExtensionPoint { embeddedLanguages: IEmbeddedLanguagesMap; tokenTypes: TokenTypesContribution; injectTo: string[]; + balancedBracketScopes: string[]; + unbalancedBracketScopes: string[]; } export const grammarsExtPoint: IExtensionPoint<ITMSyntaxExtensionPoint[]> = ExtensionsRegistry.registerExtensionPoint<ITMSyntaxExtensionPoint[]>({ @@ -64,7 +66,23 @@ export const grammarsExtPoint: IExtensionPoint<ITMSyntaxExtensionPoint[]> = Exte items: { type: 'string' } - } + }, + balancedBracketScopes: { + description: nls.localize('vscode.extension.contributes.grammars.balancedBracketScopes', 'Defines which scope names contain balanced brackets.'), + type: 'array', + items: { + type: 'string' + }, + default: ['*'], + }, + unbalancedBracketScopes: { + description: nls.localize('vscode.extension.contributes.grammars.unbalancedBracketScopes', 'Defines which scope names do not contain balanced brackets.'), + type: 'array', + items: { + type: 'string' + }, + default: [], + }, }, required: ['scopeName', 'path'] } diff --git a/src/vs/workbench/services/textMate/common/TMScopeRegistry.ts b/src/vs/workbench/services/textMate/common/TMScopeRegistry.ts index ab0be4a2f46..83b041e7823 100644 --- a/src/vs/workbench/services/textMate/common/TMScopeRegistry.ts +++ b/src/vs/workbench/services/textMate/common/TMScopeRegistry.ts @@ -15,6 +15,8 @@ export interface IValidGrammarDefinition { embeddedLanguages: IValidEmbeddedLanguagesMap; tokenTypes: IValidTokenTypeMap; injectTo?: string[]; + balancedBracketSelectors: string[]; + unbalancedBracketSelectors: string[]; } export interface IValidTokenTypeMap { diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index 02493a12664..4b5f8ac1838 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -23,7 +23,7 @@ import { IWorkingCopyBackup, WorkingCopyCapabilities, NO_TYPE_ID, IWorkingCopyBa import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { ILabelService } from 'vs/platform/label/common/label'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import { UTF8 } from 'vs/workbench/services/textfile/common/encoding'; +import { UTF16be, UTF16le, UTF8, UTF8_with_bom } from 'vs/workbench/services/textfile/common/encoding'; import { createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel'; import { ILanguageDetectionService } from 'vs/workbench/services/languageDetection/common/languageDetectionWorkerService'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; @@ -272,11 +272,11 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil //#region Resolve override async resolve(options?: ITextFileResolveOptions): Promise<void> { - this.trace('[text file model] resolve() - enter'); + this.trace('resolve() - enter'); // Return early if we are disposed if (this.isDisposed()) { - this.trace('[text file model] resolve() - exit - without resolving because model is disposed'); + this.trace('resolve() - exit - without resolving because model is disposed'); return; } @@ -285,7 +285,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // resolve a model that is dirty or is in the process of saving to prevent data // loss. if (!options?.contents && (this.dirty || this.saveSequentializer.hasPending())) { - this.trace('[text file model] resolve() - exit - without resolving because model is dirty or being saved'); + this.trace('resolve() - exit - without resolving because model is dirty or being saved'); return; } @@ -314,7 +314,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } private async resolveFromBuffer(buffer: ITextBufferFactory, options?: ITextFileResolveOptions): Promise<void> { - this.trace('[text file model] resolveFromBuffer()'); + this.trace('resolveFromBuffer()'); // Try to resolve metdata from disk let mtime: number; @@ -372,7 +372,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Abort if someone else managed to resolve the model by now let isNewModel = !this.isResolved(); if (!isNewModel) { - this.trace('[text file model] resolveFromBackup() - exit - without resolving because previously new model got created meanwhile'); + this.trace('resolveFromBackup() - exit - without resolving because previously new model got created meanwhile'); return true; // imply that resolving has happened in another operation } @@ -389,7 +389,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } private async doResolveFromBackup(backup: IResolvedWorkingCopyBackup<IBackupMetaData>, encoding: string, options?: ITextFileResolveOptions): Promise<void> { - this.trace('[text file model] doResolveFromBackup()'); + this.trace('doResolveFromBackup()'); // Resolve with backup this.resolveFromContent({ @@ -411,7 +411,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } private async resolveFromFile(options?: ITextFileResolveOptions): Promise<void> { - this.trace('[text file model] resolveFromFile()'); + this.trace('resolveFromFile()'); const forceReadFromFile = options?.forceReadFromFile; const allowBinary = this.isResolved() /* always allow if we resolved previously */ || options?.allowBinary; @@ -438,7 +438,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Return early if the model content has changed // meanwhile to prevent loosing any changes if (currentVersionId !== this.versionId) { - this.trace('[text file model] resolveFromFile() - exit - without resolving because model content changed'); + this.trace('resolveFromFile() - exit - without resolving because model content changed'); return; } @@ -475,11 +475,11 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } private resolveFromContent(content: ITextFileStreamContent, dirty: boolean, options?: ITextFileResolveOptions): void { - this.trace('[text file model] resolveFromContent() - enter'); + this.trace('resolveFromContent() - enter'); // Return early if we are disposed if (this.isDisposed()) { - this.trace('[text file model] resolveFromContent() - exit - because model is disposed'); + this.trace('resolveFromContent() - exit - because model is disposed'); return; } @@ -532,7 +532,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } private doCreateTextModel(resource: URI, value: ITextBufferFactory): void { - this.trace('[text file model] doCreateTextModel()'); + this.trace('doCreateTextModel()'); // Create model const textModel = this.createTextEditorModel(value, resource, this.preferredLanguageId); @@ -545,7 +545,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } private doUpdateTextModel(value: ITextBufferFactory): void { - this.trace('[text file model] doUpdateTextModel()'); + this.trace('doUpdateTextModel()'); // Update model value in a block that ignores content change events for dirty tracking this.ignoreDirtyOnModelContentChange = true; @@ -564,15 +564,15 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Listen to text model events this._register(model.onDidChangeContent(e => this.onModelContentChanged(model, e.isUndoing || e.isRedoing))); - this._register(model.onDidChangeLanguage(e => this.onMaybeShouldChangeEncoding())); // detect possible encoding change via language specific settings + this._register(model.onDidChangeLanguage(() => this.onMaybeShouldChangeEncoding())); // detect possible encoding change via language specific settings } private onModelContentChanged(model: ITextModel, isUndoingOrRedoing: boolean): void { - this.trace(`[text file model] onModelContentChanged() - enter`); + this.trace(`onModelContentChanged() - enter`); // In any case increment the version id because it tracks the textual content state of the model at all times this.versionId++; - this.trace(`[text file model] onModelContentChanged() - new versionId ${this.versionId}`); + this.trace(`onModelContentChanged() - new versionId ${this.versionId}`); // Remember when the user changed the model through a undo/redo operation. // We need this information to throttle save participants to fix @@ -589,7 +589,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // The contents changed as a matter of Undo and the version reached matches the saved one // In this case we clear the dirty flag and emit a SAVED event to indicate this state. if (model.getAlternativeVersionId() === this.bufferSavedVersionId) { - this.trace('[text file model] onModelContentChanged() - model content changed back to last saved version'); + this.trace('onModelContentChanged() - model content changed back to last saved version'); // Clear flags const wasDirty = this.dirty; @@ -603,7 +603,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Otherwise the content has changed and we signal this as becoming dirty else { - this.trace('[text file model] onModelContentChanged() - model content changed and marked as dirty'); + this.trace('onModelContentChanged() - model content changed and marked as dirty'); // Mark as dirty this.setDirty(true); @@ -707,7 +707,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } if (this.isReadonly()) { - this.trace('[text file model] save() - ignoring request for readonly resource'); + this.trace('save() - ignoring request for readonly resource'); return false; // if model is readonly we do not attempt to save at all } @@ -716,15 +716,15 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil (this.hasState(TextFileEditorModelState.CONFLICT) || this.hasState(TextFileEditorModelState.ERROR)) && (options.reason === SaveReason.AUTO || options.reason === SaveReason.FOCUS_CHANGE || options.reason === SaveReason.WINDOW_CHANGE) ) { - this.trace('[text file model] save() - ignoring auto save request for model that is in conflict or error'); + this.trace('save() - ignoring auto save request for model that is in conflict or error'); return false; // if model is in save conflict or error, do not save unless save reason is explicit } // Actually do save and log - this.trace('[text file model] save() - enter'); + this.trace('save() - enter'); await this.doSave(options); - this.trace('[text file model] save() - exit'); + this.trace('save() - exit'); return this.hasState(TextFileEditorModelState.SAVED); } @@ -735,7 +735,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } let versionId = this.versionId; - this.trace(`[text file model] doSave(${versionId}) - enter with versionId ${versionId}`); + this.trace(`doSave(${versionId}) - enter with versionId ${versionId}`); // Lookup any running pending save for this versionId and return it if found // @@ -743,7 +743,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // while the save was not yet finished to disk // if (this.saveSequentializer.hasPending(versionId)) { - this.trace(`[text file model] doSave(${versionId}) - exit - found a pending save for versionId ${versionId}`); + this.trace(`doSave(${versionId}) - exit - found a pending save for versionId ${versionId}`); return this.saveSequentializer.pending; } @@ -752,7 +752,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // // Scenario: user invoked save action even though the model is not dirty if (!options.force && !this.dirty) { - this.trace(`[text file model] doSave(${versionId}) - exit - because not dirty and/or versionId is different (this.isDirty: ${this.dirty}, this.versionId: ${this.versionId})`); + this.trace(`doSave(${versionId}) - exit - because not dirty and/or versionId is different (this.isDirty: ${this.dirty}, this.versionId: ${this.versionId})`); return; } @@ -766,7 +766,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // while the first save has not returned yet. // if (this.saveSequentializer.hasPending()) { - this.trace(`[text file model] doSave(${versionId}) - exit - because busy saving`); + this.trace(`doSave(${versionId}) - exit - because busy saving`); // Indicate to the save sequentializer that we want to // cancel the pending operation so that ours can run @@ -862,7 +862,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Save to Disk. We mark the save operation as currently pending with // the latest versionId because it might have changed from a save // participant triggering - this.trace(`[text file model] doSave(${versionId}) - before write()`); + this.trace(`doSave(${versionId}) - before write()`); const lastResolvedFileStat = assertIsDefined(this.lastResolvedFileStat); const resolvedTextFileEditorModel = this; return this.saveSequentializer.setPending(versionId, (async () => { @@ -890,10 +890,10 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Update dirty state unless model has changed meanwhile if (versionId === this.versionId) { - this.trace(`[text file model] handleSaveSuccess(${versionId}) - setting dirty to false because versionId did not change`); + this.trace(`handleSaveSuccess(${versionId}) - setting dirty to false because versionId did not change`); this.setDirty(false); } else { - this.trace(`[text file model] handleSaveSuccess(${versionId}) - not setting dirty to false because versionId did change meanwhile`); + this.trace(`handleSaveSuccess(${versionId}) - not setting dirty to false because versionId did change meanwhile`); } // Update orphan state given save was successful @@ -1019,15 +1019,27 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // (see https://github.com/microsoft/vscode/issues/127936) if (this.hasEncodingSetExplicitly) { + this.trace('onMaybeShouldChangeEncoding() - ignoring because encoding was set explicitly'); + return; // never change the user's choice of encoding } + if (this.contentEncoding === UTF8_with_bom || this.contentEncoding === UTF16be || this.contentEncoding === UTF16le) { + this.trace('onMaybeShouldChangeEncoding() - ignoring because content encoding has a BOM'); + + return; // never change an encoding that we can detect 100% via BOMs + } + const { encoding } = await this.textFileService.encoding.getPreferredReadEncoding(this.resource); if (typeof encoding !== 'string' || !this.isNewEncoding(encoding)) { + this.trace(`onMaybeShouldChangeEncoding() - ignoring because preferred encoding ${encoding} is not new`); + return; // return early if encoding is invalid or did not change } if (this.isDirty()) { + this.trace('onMaybeShouldChangeEncoding() - ignoring because model is dirty'); + return; // return early to prevent accident saves in this case } @@ -1110,7 +1122,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil //#endregion private trace(msg: string): void { - this.logService.trace(msg, this.resource.toString()); + this.logService.trace(`[text file model] ${msg}`, this.resource.toString()); } override isResolved(): this is IResolvedTextFileEditorModel { @@ -1122,7 +1134,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } override dispose(): void { - this.trace('[text file model] dispose()'); + this.trace('dispose()'); this.inConflictMode = false; this.inOrphanMode = false; diff --git a/src/vs/workbench/services/textfile/electron-sandbox/nativeTextFileService.ts b/src/vs/workbench/services/textfile/electron-sandbox/nativeTextFileService.ts index d4f8852280b..94cd05ed2e3 100644 --- a/src/vs/workbench/services/textfile/electron-sandbox/nativeTextFileService.ts +++ b/src/vs/workbench/services/textfile/electron-sandbox/nativeTextFileService.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from 'vs/nls'; import { process } from 'vs/base/parts/sandbox/electron-sandbox/globals'; import { AbstractTextFileService } from 'vs/workbench/services/textfile/browser/textFileService'; import { ITextFileService, ITextFileStreamContent, ITextFileContent, IReadTextFileOptions, TextFileEditorModelState, ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; @@ -63,7 +64,7 @@ export class NativeTextFileService extends AbstractTextFileService { private registerListeners(): void { // Lifecycle - this.lifecycleService.onWillShutdown(event => event.join(this.onWillShutdown(), 'join.textFiles')); + this.lifecycleService.onWillShutdown(event => event.join(this.onWillShutdown(), { id: 'join.textFiles', label: localize('join.textFiles', "Saving text files") })); } private async onWillShutdown(): Promise<void> { diff --git a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts index 6f8986a8963..901f92d9f3e 100644 --- a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts @@ -129,7 +129,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { this.fileIconThemeWatcher = new ThemeFileWatcher(fileService, environmentService, this.reloadCurrentFileIconTheme.bind(this)); this.fileIconThemeRegistry = new ThemeRegistry(fileIconThemesExtPoint, FileIconThemeData.fromExtensionTheme, true, FileIconThemeData.noIconTheme); this.fileIconThemeLoader = new FileIconThemeLoader(extensionResourceLoaderService, languageService); - this.onFileIconThemeChange = new Emitter<IWorkbenchFileIconTheme>(); + this.onFileIconThemeChange = new Emitter<IWorkbenchFileIconTheme>({ leakWarningThreshold: 400 }); this.currentFileIconTheme = FileIconThemeData.createUnloadedTheme(''); this.fileIconThemeSequencer = new Sequencer(); diff --git a/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts b/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts index 29ff6f8a218..482be496d9b 100644 --- a/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts +++ b/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts @@ -20,7 +20,6 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { localize } from 'vs/nls'; -import { canceled } from 'vs/base/common/errors'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -35,6 +34,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { UserDataSyncStoreClient } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; import { UserDataSyncStoreTypeSynchronizer } from 'vs/platform/userDataSync/common/globalStateSync'; import { ICredentialsService } from 'vs/platform/credentials/common/credentials'; +import { CancellationError } from 'vs/base/common/errors'; type UserAccountClassification = { id: { classification: 'EndUserPseudonymizedInformation'; purpose: 'BusinessInsight' }; @@ -272,7 +272,7 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat const picked = await this.pick(); if (!picked) { - throw canceled(); + throw new CancellationError(); } // User did not pick an account or login failed @@ -450,7 +450,7 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat return 'manual'; } this.telemetryService.publicLog2<{ action: string }, FirstTimeSyncClassification>('sync/firstTimeSync', { action: 'cancelled' }); - throw canceled(); + throw new CancellationError(); } private async syncManually(task: IManualSyncTask): Promise<void> { @@ -769,7 +769,7 @@ class UserDataSyncPreview extends Disposable implements IUserDataSyncPreview { } await this.manualSync.task.stop(); this.updatePreview([]); - this._onDidCompleteManualSync.fire(canceled()); + this._onDidCompleteManualSync.fire(new CancellationError()); } async pull(): Promise<void> { diff --git a/src/vs/workbench/services/views/browser/viewDescriptorService.ts b/src/vs/workbench/services/views/browser/viewDescriptorService.ts index 91dce814e18..4e84c1e5796 100644 --- a/src/vs/workbench/services/views/browser/viewDescriptorService.ts +++ b/src/vs/workbench/services/views/browser/viewDescriptorService.ts @@ -19,6 +19,7 @@ import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiati import { getViewsStateStorageId, ViewContainerModel } from 'vs/workbench/services/views/common/viewContainerModel'; import { registerAction2, Action2, MenuId } from 'vs/platform/actions/common/actions'; import { localize } from 'vs/nls'; +import { Extensions, IProfileStorageRegistry } from 'vs/workbench/services/profiles/common/profileStorageRegistry'; interface ICachedViewContainerInfo { containerId: string; @@ -142,6 +143,17 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor this._register(this.storageService.onDidChangeValue((e) => { this.onDidStorageChange(e); })); this._register(this.extensionService.onDidRegisterExtensions(() => this.onDidRegisterExtensions())); + + // Cached View Containers Locations should be registered before Cached View Positions + // Because View Containers cache should be updated first because View Positions Cache depends on View Containers Cache if views are moved to generated view containers + Registry.as<IProfileStorageRegistry>(Extensions.ProfileStorageRegistry) + .registerKeys([{ + key: ViewDescriptorService.CACHED_VIEW_CONTAINER_LOCATIONS, + description: localize('cachedViewContainerPositions', "View Container locations customizations"), + }, { + key: ViewDescriptorService.CACHED_VIEW_POSITIONS, + description: localize('cachedViewPositions', "View locations customizations"), + }]); } private registerGroupedViews(groupedViews: Map<string, { cachedContainerInfo?: ICachedViewContainerInfo; views: IViewDescriptor[] }>): void { @@ -306,7 +318,7 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor return this.viewContainersRegistry.getDefaultViewContainer(location); } - moveViewContainerToLocation(viewContainer: ViewContainer, location: ViewContainerLocation, requestedIndex?: number): void { + private doMoveViewContainerToLocation(viewContainer: ViewContainer, location: ViewContainerLocation, requestedIndex?: number, skipDiskWrite?: boolean): void { const from = this.getViewContainerLocation(viewContainer); const to = location; if (from !== to) { @@ -321,10 +333,18 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor const views = this.getViewsByContainer(viewContainer); this._onDidChangeLocation.fire({ views, from, to }); - this.saveViewContainerLocationsToCache(); + // Need to skip when syncing multiple container movements - vscode#148363 + if (!skipDiskWrite) { + this.saveViewContainerLocationsToCache(); + } } } + moveViewContainerToLocation(viewContainer: ViewContainer, location: ViewContainerLocation, requestedIndex?: number): void { + this.doMoveViewContainerToLocation(viewContainer, location, requestedIndex); + } + + moveViewToLocation(view: IViewDescriptor, location: ViewContainerLocation): void { let container = this.registerGeneratedViewContainer(location); this.moveViewsToContainer([view], container); @@ -419,11 +439,13 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor } type ViewDescriptorServiceMoveViewsClassification = { - viewCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; - fromContainer: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; - toContainer: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; - fromLocation: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; - toLocation: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; + owner: 'sbatten'; + comment: 'Logged when views are moved from one view container to another'; + viewCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of views moved' }; + fromContainer: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The starting view container of the moved views' }; + toContainer: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The destination view container of the moved views' }; + fromLocation: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The location of the starting view container. e.g. Primary Side Bar' }; + toLocation: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The location of the destination view container. e.g. Panel' }; }; this.telemetryService.publicLog2<ViewDescriptorServiceMoveViewsEvent, ViewDescriptorServiceMoveViewsClassification>('viewDescriptorService.moveViews', { viewCount, fromContainer, toContainer, fromLocation, toLocation }); @@ -508,90 +530,98 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor private onDidStorageChange(e: IStorageValueChangeEvent): void { if (e.key === ViewDescriptorService.CACHED_VIEW_POSITIONS && e.scope === StorageScope.GLOBAL && this.cachedViewPositionsValue !== this.getStoredCachedViewPositionsValue() /* This checks if current window changed the value or not */) { - this._cachedViewPositionsValue = this.getStoredCachedViewPositionsValue(); + this.onDidCachedViewPositionsStorageChange(); + } + + if (e.key === ViewDescriptorService.CACHED_VIEW_CONTAINER_LOCATIONS && e.scope === StorageScope.GLOBAL + && this.cachedViewContainerLocationsValue !== this.getStoredCachedViewContainerLocationsValue() /* This checks if current window changed the value or not */) { + this.onDidCachedViewContainerLocationsStorageChange(); + } + } - const newCachedPositions = this.getCachedViewPositions(); - const viewsToMove: { views: IViewDescriptor[]; from: ViewContainer; to: ViewContainer }[] = []; + private onDidCachedViewPositionsStorageChange(): void { + this._cachedViewPositionsValue = this.getStoredCachedViewPositionsValue(); - for (let viewId of newCachedPositions.keys()) { - const viewDescriptor = this.getViewDescriptorById(viewId); - if (!viewDescriptor) { - continue; - } + const newCachedPositions = this.getCachedViewPositions(); + const viewsToMove: { views: IViewDescriptor[]; from: ViewContainer; to: ViewContainer }[] = []; - const prevViewContainer = this.getViewContainerByViewId(viewId); - const newViewContainerInfo = newCachedPositions.get(viewId)!; - // Verify if we need to create the destination container - if (!this.viewContainersRegistry.get(newViewContainerInfo.containerId)) { - const location = this.cachedViewContainerInfo.get(newViewContainerInfo.containerId); - if (location !== undefined) { - this.registerGeneratedViewContainer(location, newViewContainerInfo.containerId); - } + for (let viewId of newCachedPositions.keys()) { + const viewDescriptor = this.getViewDescriptorById(viewId); + if (!viewDescriptor) { + continue; + } + + const prevViewContainer = this.getViewContainerByViewId(viewId); + const newViewContainerInfo = newCachedPositions.get(viewId)!; + // Verify if we need to create the destination container + if (!this.viewContainersRegistry.get(newViewContainerInfo.containerId)) { + const location = this.cachedViewContainerInfo.get(newViewContainerInfo.containerId); + if (location !== undefined) { + this.registerGeneratedViewContainer(location, newViewContainerInfo.containerId); } + } - // Try moving to the new container - const newViewContainer = this.viewContainersRegistry.get(newViewContainerInfo.containerId); - if (prevViewContainer && newViewContainer && newViewContainer !== prevViewContainer) { - const viewDescriptor = this.getViewDescriptorById(viewId); - if (viewDescriptor) { - viewsToMove.push({ views: [viewDescriptor], from: prevViewContainer, to: newViewContainer }); - } + // Try moving to the new container + const newViewContainer = this.viewContainersRegistry.get(newViewContainerInfo.containerId); + if (prevViewContainer && newViewContainer && newViewContainer !== prevViewContainer) { + const viewDescriptor = this.getViewDescriptorById(viewId); + if (viewDescriptor) { + viewsToMove.push({ views: [viewDescriptor], from: prevViewContainer, to: newViewContainer }); } } + } - // If a value is not present in the cache, it must be reset to default - this.viewContainers.forEach(viewContainer => { - const viewContainerModel = this.getViewContainerModel(viewContainer); - viewContainerModel.allViewDescriptors.forEach(viewDescriptor => { - if (!newCachedPositions.has(viewDescriptor.id)) { - const currentContainer = this.getViewContainerByViewId(viewDescriptor.id); - const defaultContainer = this.getDefaultContainerById(viewDescriptor.id); - if (currentContainer && defaultContainer && currentContainer !== defaultContainer) { - viewsToMove.push({ views: [viewDescriptor], from: currentContainer, to: defaultContainer }); - } + // If a value is not present in the cache, it must be reset to default + this.viewContainers.forEach(viewContainer => { + const viewContainerModel = this.getViewContainerModel(viewContainer); + viewContainerModel.allViewDescriptors.forEach(viewDescriptor => { + if (!newCachedPositions.has(viewDescriptor.id)) { + const currentContainer = this.getViewContainerByViewId(viewDescriptor.id); + const defaultContainer = this.getDefaultContainerById(viewDescriptor.id); + if (currentContainer && defaultContainer && currentContainer !== defaultContainer) { + viewsToMove.push({ views: [viewDescriptor], from: currentContainer, to: defaultContainer }); } - }); + } }); + }); - this.cachedViewInfo = newCachedPositions; - for (const { views, from, to } of viewsToMove) { - this.moveViews(views, from, to); - } + this.cachedViewInfo = newCachedPositions; + for (const { views, from, to } of viewsToMove) { + this.moveViews(views, from, to); } + } - if (e.key === ViewDescriptorService.CACHED_VIEW_CONTAINER_LOCATIONS && e.scope === StorageScope.GLOBAL - && this.cachedViewContainerLocationsValue !== this.getStoredCachedViewContainerLocationsValue() /* This checks if current window changed the value or not */) { - this._cachedViewContainerLocationsValue = this.getStoredCachedViewContainerLocationsValue(); - const newCachedLocations = this.getCachedViewContainerLocations(); - const viewContainersToMove: [ViewContainer, ViewContainerLocation][] = []; - - for (const [containerId, location] of newCachedLocations.entries()) { - const container = this.getViewContainerById(containerId); - if (container) { - if (location !== this.getViewContainerLocation(container)) { - viewContainersToMove.push([container, location]); - } + private onDidCachedViewContainerLocationsStorageChange(): void { + this._cachedViewContainerLocationsValue = this.getStoredCachedViewContainerLocationsValue(); + const newCachedLocations = this.getCachedViewContainerLocations(); + const viewContainersToMove: [ViewContainer, ViewContainerLocation][] = []; + + for (const [containerId, location] of newCachedLocations.entries()) { + const container = this.getViewContainerById(containerId); + if (container) { + if (location !== this.getViewContainerLocation(container)) { + viewContainersToMove.push([container, location]); } } + } - this.viewContainers.forEach(viewContainer => { - if (!newCachedLocations.has(viewContainer.id)) { - const currentLocation = this.getViewContainerLocation(viewContainer); - const defaultLocation = this.getDefaultViewContainerLocation(viewContainer); + this.viewContainers.forEach(viewContainer => { + if (!newCachedLocations.has(viewContainer.id)) { + const currentLocation = this.getViewContainerLocation(viewContainer); + const defaultLocation = this.getDefaultViewContainerLocation(viewContainer); - if (currentLocation !== defaultLocation) { - viewContainersToMove.push([viewContainer, defaultLocation]); - } + if (currentLocation !== defaultLocation) { + viewContainersToMove.push([viewContainer, defaultLocation]); } - }); - - // Execute View Container Movement - for (const [container, location] of viewContainersToMove) { - this.moveViewContainerToLocation(container, location); } + }); - this.cachedViewContainerInfo = this.getCachedViewContainerLocations(); + // Execute View Container Movement + for (const [container, location] of viewContainersToMove) { + this.doMoveViewContainerToLocation(container, location, undefined, true); } + + this.cachedViewContainerInfo = this.getCachedViewContainerLocations(); } // Generated Container Id Format diff --git a/src/vs/workbench/services/views/common/viewContainerModel.ts b/src/vs/workbench/services/views/common/viewContainerModel.ts index 00965871db3..5cb12926dc9 100644 --- a/src/vs/workbench/services/views/common/viewContainerModel.ts +++ b/src/vs/workbench/services/views/common/viewContainerModel.ts @@ -16,6 +16,8 @@ import { isUndefined, isUndefinedOrNull } from 'vs/base/common/types'; import { isEqual } from 'vs/base/common/resources'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { IStringDictionary } from 'vs/base/common/collections'; +import { Extensions, IProfileStorageRegistry } from 'vs/workbench/services/profiles/common/profileStorageRegistry'; +import { localize } from 'vs/nls'; export function getViewsStateStorageId(viewContainerStorageId: string): string { return `${viewContainerStorageId}.hidden`; } @@ -84,6 +86,7 @@ class ViewDescriptorsState extends Disposable { constructor( viewContainerStorageId: string, + viewContainerName: string, @IStorageService private readonly storageService: IStorageService, ) { super(); @@ -93,6 +96,12 @@ class ViewDescriptorsState extends Disposable { this._register(this.storageService.onDidChangeValue(e => this.onDidStorageChange(e))); this.state = this.initialize(); + + Registry.as<IProfileStorageRegistry>(Extensions.ProfileStorageRegistry) + .registerKeys([{ + key: this.globalViewsStateStorageId, + description: localize('globalViewsStateStorageId', "Views visibility customizations in {0} view container", viewContainerName), + }]); } set(id: string, state: IViewDescriptorState): void { @@ -343,7 +352,7 @@ export class ViewContainerModel extends Disposable implements IViewContainerMode super(); this._register(Event.filter(contextKeyService.onDidChangeContext, e => e.affectsSome(this.contextKeys))(() => this.onDidChangeContext())); - this.viewDescriptorsState = this._register(instantiationService.createInstance(ViewDescriptorsState, viewContainer.storageId || `${viewContainer.id}.state`)); + this.viewDescriptorsState = this._register(instantiationService.createInstance(ViewDescriptorsState, viewContainer.storageId || `${viewContainer.id}.state`, viewContainer.title)); this._register(this.viewDescriptorsState.onDidChangeStoredState(items => this.updateVisibility(items))); this._register(Event.any( diff --git a/src/vs/workbench/services/workingCopy/browser/workingCopyHistoryService.ts b/src/vs/workbench/services/workingCopy/browser/workingCopyHistoryService.ts index 2ada834a567..5b620f0b1e8 100644 --- a/src/vs/workbench/services/workingCopy/browser/workingCopyHistoryService.ts +++ b/src/vs/workbench/services/workingCopy/browser/workingCopyHistoryService.ts @@ -3,54 +3,23 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URI } from 'vs/base/common/uri'; -import { SaveSource } from 'vs/workbench/common/editor'; -import { CancellationToken } from 'vs/base/common/cancellation'; import { IFileService } from 'vs/platform/files/common/files'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { ILabelService } from 'vs/platform/label/common/label'; import { ILogService } from 'vs/platform/log/common/log'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { WorkingCopyHistoryModel, WorkingCopyHistoryService } from 'vs/workbench/services/workingCopy/common/workingCopyHistoryService'; +import { IWorkingCopyHistoryModelOptions, WorkingCopyHistoryService } from 'vs/workbench/services/workingCopy/common/workingCopyHistoryService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { IWorkingCopyHistoryEntry, IWorkingCopyHistoryService } from 'vs/workbench/services/workingCopy/common/workingCopyHistory'; - -class BrowserWorkingCopyHistoryModel extends WorkingCopyHistoryModel { - - override async addEntry(source: SaveSource, timestamp: number, token: CancellationToken): Promise<IWorkingCopyHistoryEntry> { - const entry = await super.addEntry(source, timestamp, token); - if (!token.isCancellationRequested) { - await this.store(token); // need to store on each add because we do not have long running shutdown support in web - } - - return entry; - } - - override async updateEntry(entry: IWorkingCopyHistoryEntry, properties: { source: SaveSource }, token: CancellationToken): Promise<void> { - await super.updateEntry(entry, properties, token); - if (!token.isCancellationRequested) { - await this.store(token); // need to store on each remove because we do not have long running shutdown support in web - } - } - - override async removeEntry(entry: IWorkingCopyHistoryEntry, token: CancellationToken): Promise<boolean> { - const removed = await super.removeEntry(entry, token); - if (removed && !token.isCancellationRequested) { - await this.store(token); // need to store on each remove because we do not have long running shutdown support in web - } - - return removed; - } -} +import { IWorkingCopyHistoryService } from 'vs/workbench/services/workingCopy/common/workingCopyHistory'; export class BrowserWorkingCopyHistoryService extends WorkingCopyHistoryService { constructor( @IFileService fileService: IFileService, @IRemoteAgentService remoteAgentService: IRemoteAgentService, - @IEnvironmentService environmentService: IEnvironmentService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, @IUriIdentityService uriIdentityService: IUriIdentityService, @ILabelService labelService: ILabelService, @ILogService logService: ILogService, @@ -59,8 +28,8 @@ export class BrowserWorkingCopyHistoryService extends WorkingCopyHistoryService super(fileService, remoteAgentService, environmentService, uriIdentityService, labelService, logService, configurationService); } - protected override createModel(resource: URI, historyHome: URI): WorkingCopyHistoryModel { - return new BrowserWorkingCopyHistoryModel(resource, historyHome, this._onDidAddEntry, this._onDidChangeEntry, this._onDidReplaceEntry, this._onDidRemoveEntry, this.fileService, this.labelService, this.logService, this.configurationService); + protected getModelOptions(): IWorkingCopyHistoryModelOptions { + return { flushOnChange: true /* because browsers support no long running shutdown */ }; } } diff --git a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts index 15b31899685..ff9b4a7c408 100644 --- a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts @@ -405,11 +405,11 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend } async resolve(options?: IStoredFileWorkingCopyResolveOptions): Promise<void> { - this.trace('[stored file working copy] resolve() - enter'); + this.trace('resolve() - enter'); // Return early if we are disposed if (this.isDisposed()) { - this.trace('[stored file working copy] resolve() - exit - without resolving because file working copy is disposed'); + this.trace('resolve() - exit - without resolving because file working copy is disposed'); return; } @@ -418,7 +418,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend // resolve a working copy that is dirty or is in the process of saving to prevent // data loss. if (!options?.contents && (this.dirty || this.saveSequentializer.hasPending())) { - this.trace('[stored file working copy] resolve() - exit - without resolving because file working copy is dirty or being saved'); + this.trace('resolve() - exit - without resolving because file working copy is dirty or being saved'); return; } @@ -447,7 +447,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend } private async resolveFromBuffer(buffer: VSBufferReadableStream): Promise<void> { - this.trace('[stored file working copy] resolveFromBuffer()'); + this.trace('resolveFromBuffer()'); // Try to resolve metdata from disk let mtime: number; @@ -496,7 +496,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend // Abort if someone else managed to resolve the working copy by now let isNew = !this.isResolved(); if (!isNew) { - this.trace('[stored file working copy] resolveFromBackup() - exit - withoutresolving because previously new file working copy got created meanwhile'); + this.trace('resolveFromBackup() - exit - withoutresolving because previously new file working copy got created meanwhile'); return true; // imply that resolving has happened in another operation } @@ -513,7 +513,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend } private async doResolveFromBackup(backup: IResolvedWorkingCopyBackup<IStoredFileWorkingCopyBackupMetaData>): Promise<void> { - this.trace('[stored file working copy] doResolveFromBackup()'); + this.trace('doResolveFromBackup()'); // Resolve with backup await this.resolveFromContent({ @@ -534,7 +534,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend } private async resolveFromFile(options?: IStoredFileWorkingCopyResolveOptions): Promise<void> { - this.trace('[stored file working copy] resolveFromFile()'); + this.trace('resolveFromFile()'); const forceReadFromFile = options?.forceReadFromFile; @@ -561,7 +561,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend // Return early if the working copy content has changed // meanwhile to prevent loosing any changes if (currentVersionId !== this.versionId) { - this.trace('[stored file working copy] resolveFromFile() - exit - without resolving because file working copy content changed'); + this.trace('resolveFromFile() - exit - without resolving because file working copy content changed'); return; } @@ -598,11 +598,11 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend } private async resolveFromContent(content: IFileStreamContent, dirty: boolean): Promise<void> { - this.trace('[stored file working copy] resolveFromContent() - enter'); + this.trace('resolveFromContent() - enter'); // Return early if we are disposed if (this.isDisposed()) { - this.trace('[stored file working copy] resolveFromContent() - exit - because working copy is disposed'); + this.trace('resolveFromContent() - exit - because working copy is disposed'); return; } @@ -644,7 +644,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend } private async doCreateModel(contents: VSBufferReadableStream): Promise<void> { - this.trace('[stored file working copy] doCreateModel()'); + this.trace('doCreateModel()'); // Create model and dispose it when we get disposed this._model = this._register(await this.modelFactory.createModel(this.resource, contents, CancellationToken.None)); @@ -656,7 +656,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend private ignoreDirtyOnModelContentChange = false; private async doUpdateModel(contents: VSBufferReadableStream): Promise<void> { - this.trace('[stored file working copy] doUpdateModel()'); + this.trace('doUpdateModel()'); // Update model value in a block that ignores content change events for dirty tracking this.ignoreDirtyOnModelContentChange = true; @@ -681,11 +681,11 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend } private onModelContentChanged(model: M, isUndoingOrRedoing: boolean): void { - this.trace(`[stored file working copy] onModelContentChanged() - enter`); + this.trace(`onModelContentChanged() - enter`); // In any case increment the version id because it tracks the content state of the model at all times this.versionId++; - this.trace(`[stored file working copy] onModelContentChanged() - new versionId ${this.versionId}`); + this.trace(`onModelContentChanged() - new versionId ${this.versionId}`); // Remember when the user changed the model through a undo/redo operation. // We need this information to throttle save participants to fix @@ -702,7 +702,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend // The contents changed as a matter of Undo and the version reached matches the saved one // In this case we clear the dirty flag and emit a SAVED event to indicate this state. if (model.versionId === this.savedVersionId) { - this.trace('[stored file working copy] onModelContentChanged() - model content changed back to last saved version'); + this.trace('onModelContentChanged() - model content changed back to last saved version'); // Clear flags const wasDirty = this.dirty; @@ -716,7 +716,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend // Otherwise the content has changed and we signal this as becoming dirty else { - this.trace('[stored file working copy] onModelContentChanged() - model content changed and marked as dirty'); + this.trace('onModelContentChanged() - model content changed and marked as dirty'); // Mark as dirty this.setDirty(true); @@ -787,7 +787,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend } if (this.isReadonly()) { - this.trace('[stored file working copy] save() - ignoring request for readonly resource'); + this.trace('save() - ignoring request for readonly resource'); return false; // if working copy is readonly we do not attempt to save at all } @@ -796,15 +796,15 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend (this.hasState(StoredFileWorkingCopyState.CONFLICT) || this.hasState(StoredFileWorkingCopyState.ERROR)) && (options.reason === SaveReason.AUTO || options.reason === SaveReason.FOCUS_CHANGE || options.reason === SaveReason.WINDOW_CHANGE) ) { - this.trace('[stored file working copy] save() - ignoring auto save request for file working copy that is in conflict or error'); + this.trace('save() - ignoring auto save request for file working copy that is in conflict or error'); return false; // if working copy is in save conflict or error, do not save unless save reason is explicit } // Actually do save - this.trace('[stored file working copy] save() - enter'); + this.trace('save() - enter'); await this.doSave(options); - this.trace('[stored file working copy] save() - exit'); + this.trace('save() - exit'); return this.hasState(StoredFileWorkingCopyState.SAVED); } @@ -815,7 +815,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend } let versionId = this.versionId; - this.trace(`[stored file working copy] doSave(${versionId}) - enter with versionId ${versionId}`); + this.trace(`doSave(${versionId}) - enter with versionId ${versionId}`); // Lookup any running pending save for this versionId and return it if found // @@ -823,7 +823,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend // while the save was not yet finished to disk // if (this.saveSequentializer.hasPending(versionId)) { - this.trace(`[stored file working copy] doSave(${versionId}) - exit - found a pending save for versionId ${versionId}`); + this.trace(`doSave(${versionId}) - exit - found a pending save for versionId ${versionId}`); return this.saveSequentializer.pending; } @@ -832,7 +832,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend // // Scenario: user invoked save action even though the working copy is not dirty if (!options.force && !this.dirty) { - this.trace(`[stored file working copy] doSave(${versionId}) - exit - because not dirty and/or versionId is different (this.isDirty: ${this.dirty}, this.versionId: ${this.versionId})`); + this.trace(`doSave(${versionId}) - exit - because not dirty and/or versionId is different (this.isDirty: ${this.dirty}, this.versionId: ${this.versionId})`); return; } @@ -846,7 +846,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend // while the first save has not returned yet. // if (this.saveSequentializer.hasPending()) { - this.trace(`[stored file working copy] doSave(${versionId}) - exit - because busy saving`); + this.trace(`doSave(${versionId}) - exit - because busy saving`); // Indicate to the save sequentializer that we want to // cancel the pending operation so that ours can run @@ -939,7 +939,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend // Save to Disk. We mark the save operation as currently pending with // the latest versionId because it might have changed from a save // participant triggering - this.trace(`[stored file working copy] doSave(${versionId}) - before write()`); + this.trace(`doSave(${versionId}) - before write()`); const lastResolvedFileStat = assertIsDefined(this.lastResolvedFileStat); const resolvedFileWorkingCopy = this; return this.saveSequentializer.setPending(versionId, (async () => { @@ -989,10 +989,10 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend // Update dirty state unless working copy has changed meanwhile if (versionId === this.versionId) { - this.trace(`[stored file working copy] handleSaveSuccess(${versionId}) - setting dirty to false because versionId did not change`); + this.trace(`handleSaveSuccess(${versionId}) - setting dirty to false because versionId did not change`); this.setDirty(false); } else { - this.trace(`[stored file working copy] handleSaveSuccess(${versionId}) - not setting dirty to false because versionId did change meanwhile`); + this.trace(`handleSaveSuccess(${versionId}) - not setting dirty to false because versionId did change meanwhile`); } // Update orphan state given save was successful @@ -1003,7 +1003,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend } private handleSaveError(error: Error, versionId: number, options: IStoredFileWorkingCopySaveOptions): void { - (options.ignoreErrorHandler ? this.logService.trace : this.logService.error)(`[stored file working copy] handleSaveError(${versionId}) - exit - resulted in a save error: ${error.toString()}`, this.resource.toString(), this.typeId); + (options.ignoreErrorHandler ? this.logService.trace : this.logService.error).apply(this.logService, [`[stored file working copy] handleSaveError(${versionId}) - exit - resulted in a save error: ${error.toString()}`, this.resource.toString(), this.typeId]); // Return early if the save() call was made asking to // handle the save error itself. @@ -1149,7 +1149,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend return; // ignore if not resolved or not dirty and not enforced } - this.trace('[stored file working copy] revert()'); + this.trace('revert()'); // Unset flags const wasDirty = this.dirty; @@ -1219,7 +1219,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend } private trace(msg: string): void { - this.logService.trace(msg, this.resource.toString(), this.typeId); + this.logService.trace(`[stored file working copy] ${msg}`, this.resource.toString(), this.typeId); } //#endregion @@ -1227,7 +1227,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend //#region Dispose override dispose(): void { - this.trace('[stored file working copy] dispose()'); + this.trace('dispose()'); // State this.inConflictMode = false; diff --git a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts index b92776c6c5a..d7c4f83dcc0 100644 --- a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from 'vs/nls'; import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; import { StoredFileWorkingCopy, StoredFileWorkingCopyState, IStoredFileWorkingCopy, IStoredFileWorkingCopyModel, IStoredFileWorkingCopyModelFactory, IStoredFileWorkingCopyResolveOptions, IStoredFileWorkingCopySaveEvent as IBaseStoredFileWorkingCopySaveEvent } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopy'; @@ -207,7 +208,7 @@ export class StoredFileWorkingCopyManager<M extends IStoredFileWorkingCopyModel> // Lifecycle this.lifecycleService.onBeforeShutdown(event => event.veto(this.onBeforeShutdown(), 'veto.fileWorkingCopyManager')); - this.lifecycleService.onWillShutdown(event => event.join(this.onWillShutdown(), 'join.fileWorkingCopyManager')); + this.lifecycleService.onWillShutdown(event => event.join(this.onWillShutdown(), { id: 'join.fileWorkingCopyManager', label: localize('join.fileWorkingCopyManager', "Saving working copies") })); } private onBeforeShutdown(): boolean { diff --git a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts index baa8d0587d4..59d79abeefd 100644 --- a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts @@ -157,10 +157,10 @@ export class UntitledFileWorkingCopy<M extends IUntitledFileWorkingCopyModel> ex //#region Resolve async resolve(): Promise<void> { - this.trace('[untitled file working copy] resolve()'); + this.trace('resolve()'); if (this.isResolved()) { - this.trace('[untitled file working copy] resolve() - exit (already resolved)'); + this.trace('resolve() - exit (already resolved)'); // return early if the untitled file working copy is already // resolved assuming that the contents have meanwhile changed @@ -173,15 +173,15 @@ export class UntitledFileWorkingCopy<M extends IUntitledFileWorkingCopyModel> ex // Check for backups or use initial value or empty const backup = await this.workingCopyBackupService.resolve(this); if (backup) { - this.trace('[untitled file working copy] resolve() - with backup'); + this.trace('resolve() - with backup'); untitledContents = backup.value; } else if (this.initialContents?.value) { - this.trace('[untitled file working copy] resolve() - with initial contents'); + this.trace('resolve() - with initial contents'); untitledContents = this.initialContents.value; } else { - this.trace('[untitled file working copy] resolve() - empty'); + this.trace('resolve() - empty'); untitledContents = emptyStream(); } @@ -200,7 +200,7 @@ export class UntitledFileWorkingCopy<M extends IUntitledFileWorkingCopyModel> ex } private async doCreateModel(contents: VSBufferReadableStream): Promise<void> { - this.trace('[untitled file working copy] doCreateModel()'); + this.trace('doCreateModel()'); // Create model and dispose it when we get disposed this._model = this._register(await this.modelFactory.createModel(this.resource, contents, CancellationToken.None)); @@ -267,7 +267,7 @@ export class UntitledFileWorkingCopy<M extends IUntitledFileWorkingCopyModel> ex //#region Save async save(options?: ISaveOptions): Promise<boolean> { - this.trace('[untitled file working copy] save()'); + this.trace('save()'); const result = await this.saveDelegate(this, options); @@ -285,7 +285,7 @@ export class UntitledFileWorkingCopy<M extends IUntitledFileWorkingCopyModel> ex //#region Revert async revert(): Promise<void> { - this.trace('[untitled file working copy] revert()'); + this.trace('revert()'); // No longer dirty this.setDirty(false); @@ -302,7 +302,7 @@ export class UntitledFileWorkingCopy<M extends IUntitledFileWorkingCopyModel> ex //#endregion override dispose(): void { - this.trace('[untitled file working copy] dispose()'); + this.trace('dispose()'); this._onWillDispose.fire(); @@ -310,6 +310,6 @@ export class UntitledFileWorkingCopy<M extends IUntitledFileWorkingCopyModel> ex } private trace(msg: string): void { - this.logService.trace(msg, this.resource.toString(), this.typeId); + this.logService.trace(`[untitled file working copy] ${msg}`, this.resource.toString(), this.typeId); } } diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyHistoryService.ts b/src/vs/workbench/services/workingCopy/common/workingCopyHistoryService.ts index 946c595596d..667440f7070 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopyHistoryService.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyHistoryService.ts @@ -17,7 +17,7 @@ import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteA import { URI } from 'vs/base/common/uri'; import { DeferredPromise, Limiter } from 'vs/base/common/async'; import { dirname, extname, isEqual, joinPath } from 'vs/base/common/resources'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { hash } from 'vs/base/common/hash'; import { indexOfPath, randomPath } from 'vs/base/common/extpath'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -42,6 +42,17 @@ interface ISerializedWorkingCopyHistoryModelEntry { readonly source?: SaveSource; } + +export interface IWorkingCopyHistoryModelOptions { + + /** + * Whether to flush when the model changes. If not + * configured, `model.store()` has to be called + * explicitly. + */ + flushOnChange: boolean; +} + export class WorkingCopyHistoryModel { static readonly ENTRIES_FILE = 'entries.json'; @@ -65,7 +76,10 @@ export class WorkingCopyHistoryModel { private historyEntriesNameMatcher: RegExp | undefined = undefined; - private shouldStore: boolean = false; + private versionId = 0; + private storedVersionId = this.versionId; + + private readonly storeLimiter = new Limiter(1); constructor( workingCopyResource: URI, @@ -74,6 +88,7 @@ export class WorkingCopyHistoryModel { private readonly entryChangedEmitter: Emitter<IWorkingCopyHistoryEvent>, private readonly entryReplacedEmitter: Emitter<IWorkingCopyHistoryEvent>, private readonly entryRemovedEmitter: Emitter<IWorkingCopyHistoryEvent>, + private readonly options: IWorkingCopyHistoryModelOptions, private readonly fileService: IFileService, private readonly labelService: ILabelService, private readonly logService: ILogService, @@ -118,15 +133,24 @@ export class WorkingCopyHistoryModel { } } + let entry: IWorkingCopyHistoryEntry; + // Replace lastest entry in history if (entryToReplace) { - return this.doReplaceEntry(entryToReplace, timestamp, token); + entry = await this.doReplaceEntry(entryToReplace, timestamp, token); } // Add entry to history else { - return this.doAddEntry(source, timestamp, token); + entry = await this.doAddEntry(source, timestamp, token); + } + + // Flush now if configured + if (this.options.flushOnChange && !token.isCancellationRequested) { + await this.store(token); } + + return entry; } private async doAddEntry(source: SaveSource, timestamp: number, token: CancellationToken): Promise<IWorkingCopyHistoryEntry> { @@ -149,8 +173,8 @@ export class WorkingCopyHistoryModel { }; this.entries.push(entry); - // Mark as in need to be stored to disk - this.shouldStore = true; + // Update version ID of model to use for storing later + this.versionId++; // Events this.entryAddedEmitter.fire({ entry }); @@ -167,8 +191,8 @@ export class WorkingCopyHistoryModel { // Update entry entry.timestamp = timestamp; - // Mark as in need to be stored to disk - this.shouldStore = true; + // Update version ID of model to use for storing later + this.versionId++; // Events this.entryReplacedEmitter.fire({ entry }); @@ -196,12 +220,17 @@ export class WorkingCopyHistoryModel { // Remove from model this.entries.splice(index, 1); - // Mark as in need to be stored to disk - this.shouldStore = true; + // Update version ID of model to use for storing later + this.versionId++; // Events this.entryRemovedEmitter.fire({ entry }); + // Flush now if configured + if (this.options.flushOnChange && !token.isCancellationRequested) { + await this.store(token); + } + return true; } @@ -222,11 +251,16 @@ export class WorkingCopyHistoryModel { // Update entry entry.source = properties.source; - // Mark as in need to be stored to disk - this.shouldStore = true; + // Update version ID of model to use for storing later + this.versionId++; // Events this.entryChangedEmitter.fire({ entry }); + + // Flush now if configured + if (this.options.flushOnChange && !token.isCancellationRequested) { + await this.store(token); + } } async getEntries(): Promise<readonly IWorkingCopyHistoryEntry[]> { @@ -353,12 +387,29 @@ export class WorkingCopyHistoryModel { } async store(token: CancellationToken): Promise<void> { - const historyEntriesFolder = assertIsDefined(this.historyEntriesFolder); - - if (!this.shouldStore) { - return; // fast return to avoid disk access when nothing changed + if (!this.shouldStore()) { + return; } + // Use a `Limiter` to prevent multiple `store` operations + // potentially running at the same time + + await this.storeLimiter.queue(async () => { + if (token.isCancellationRequested || !this.shouldStore()) { + return; + } + + return this.doStore(token); + }); + } + + private shouldStore(): boolean { + return this.storedVersionId !== this.versionId; + } + + private async doStore(token: CancellationToken): Promise<void> { + const historyEntriesFolder = assertIsDefined(this.historyEntriesFolder); + // Make sure to await resolving when persisting await this.resolveEntriesOnce(); @@ -370,6 +421,7 @@ export class WorkingCopyHistoryModel { await this.cleanUpEntries(); // Without entries, remove the history folder + const storedVersion = this.versionId; if (this.entries.length === 0) { try { await this.fileService.del(historyEntriesFolder, { recursive: true }); @@ -383,8 +435,8 @@ export class WorkingCopyHistoryModel { await this.writeEntriesFile(); } - // Mark as being up to date on disk - this.shouldStore = false; + // Mark as stored version + this.storedVersionId = storedVersion; } private async cleanUpEntries(): Promise<void> { @@ -516,7 +568,7 @@ export abstract class WorkingCopyHistoryService extends Disposable implements IW constructor( @IFileService protected readonly fileService: IFileService, @IRemoteAgentService protected readonly remoteAgentService: IRemoteAgentService, - @IEnvironmentService protected readonly environmentService: IEnvironmentService, + @IWorkbenchEnvironmentService protected readonly environmentService: IWorkbenchEnvironmentService, @IUriIdentityService protected readonly uriIdentityService: IUriIdentityService, @ILabelService protected readonly labelService: ILabelService, @ILogService protected readonly logService: ILogService, @@ -722,16 +774,15 @@ export abstract class WorkingCopyHistoryService extends Disposable implements IW let model = this.models.get(resource); if (!model) { - model = this.createModel(resource, historyHome); + model = new WorkingCopyHistoryModel(resource, historyHome, this._onDidAddEntry, this._onDidChangeEntry, this._onDidReplaceEntry, this._onDidRemoveEntry, this.getModelOptions(), this.fileService, this.labelService, this.logService, this.configurationService); this.models.set(resource, model); } return model; } - protected createModel(resource: URI, historyHome: URI): WorkingCopyHistoryModel { - return new WorkingCopyHistoryModel(resource, historyHome, this._onDidAddEntry, this._onDidChangeEntry, this._onDidReplaceEntry, this._onDidRemoveEntry, this.fileService, this.labelService, this.logService, this.configurationService); - } + protected abstract getModelOptions(): IWorkingCopyHistoryModelOptions; + } // Register History Tracker diff --git a/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupService.ts b/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupService.ts index d4c07c0f67a..d9fd62ac9b4 100644 --- a/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupService.ts +++ b/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupService.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from 'vs/nls'; import { WorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackupService'; import { URI } from 'vs/base/common/uri'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; @@ -33,7 +34,7 @@ export class NativeWorkingCopyBackupService extends WorkingCopyBackupService { // Lifecycle: ensure to prolong the shutdown for as long // as pending backup operations have not finished yet. // Otherwise, we risk writing partial backups to disk. - this.lifecycleService.onWillShutdown(event => event.join(this.joinBackups(), 'join.workingCopyBackups')); + this.lifecycleService.onWillShutdown(event => event.join(this.joinBackups(), { id: 'join.workingCopyBackups', label: localize('join.workingCopyBackups', "Backup working copies") })); } } diff --git a/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyHistoryService.ts b/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyHistoryService.ts index 36aae715453..55167103df8 100644 --- a/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyHistoryService.ts +++ b/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyHistoryService.ts @@ -3,25 +3,35 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Limiter } from 'vs/base/common/async'; +import { localize } from 'vs/nls'; +import { Event } from 'vs/base/common/event'; +import { Limiter, RunOnceScheduler } from 'vs/base/common/async'; import { ILifecycleService, WillShutdownEvent } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IFileService } from 'vs/platform/files/common/files'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { ILabelService } from 'vs/platform/label/common/label'; import { ILogService } from 'vs/platform/log/common/log'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { WorkingCopyHistoryService } from 'vs/workbench/services/workingCopy/common/workingCopyHistoryService'; +import { IWorkingCopyHistoryModelOptions, WorkingCopyHistoryService } from 'vs/workbench/services/workingCopy/common/workingCopyHistoryService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IWorkingCopyHistoryService, MAX_PARALLEL_HISTORY_IO_OPS } from 'vs/workbench/services/workingCopy/common/workingCopyHistory'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; export class NativeWorkingCopyHistoryService extends WorkingCopyHistoryService { + private static readonly STORE_ALL_INTERVAL = 5 * 60 * 1000; // 5min + + private readonly isRemotelyStored = typeof this.environmentService.remoteAuthority === 'string'; + + private readonly storeAllCts = this._register(new CancellationTokenSource()); + private readonly storeAllScheduler = this._register(new RunOnceScheduler(() => this.storeAll(this.storeAllCts.token), NativeWorkingCopyHistoryService.STORE_ALL_INTERVAL)); + constructor( @IFileService fileService: IFileService, @IRemoteAgentService remoteAgentService: IRemoteAgentService, - @IEnvironmentService environmentService: IEnvironmentService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, @IUriIdentityService uriIdentityService: IUriIdentityService, @ILabelService labelService: ILabelService, @ILifecycleService private readonly lifecycleService: ILifecycleService, @@ -30,33 +40,60 @@ export class NativeWorkingCopyHistoryService extends WorkingCopyHistoryService { ) { super(fileService, remoteAgentService, environmentService, uriIdentityService, labelService, logService, configurationService); - this.lifecycleService.onWillShutdown(e => this.onWillShutdown(e)); + this.registerListeners(); + } + + private registerListeners(): void { + if (!this.isRemotelyStored) { + + // Local: persist all on shutdown + this.lifecycleService.onWillShutdown(e => this.onWillShutdown(e)); + + // Local: schedule persist on change + this._register(Event.any(this.onDidAddEntry, this.onDidChangeEntry, this.onDidReplaceEntry, this.onDidRemoveEntry)(() => this.onDidChangeModels())); + } + } + + protected getModelOptions(): IWorkingCopyHistoryModelOptions { + return { flushOnChange: this.isRemotelyStored /* because the connection might drop anytime */ }; } private onWillShutdown(e: WillShutdownEvent): void { - // Prolong shutdown for orderly model shutdown - e.join((async () => { - const limiter = new Limiter(MAX_PARALLEL_HISTORY_IO_OPS); - const promises = []; - - const models = Array.from(this.models.values()); - for (const model of models) { - promises.push(limiter.queue(async () => { - if (e.token.isCancellationRequested) { - return; - } - - try { - await model.store(e.token); - } catch (error) { - this.logService.trace(error); - } - })); - } - - await Promise.all(promises); - })(), 'join.workingCopyHistory'); + // Dispose the scheduler... + this.storeAllScheduler.dispose(); + this.storeAllCts.dispose(true); + + // ...because we now explicitly store all models + e.join(this.storeAll(e.token), { id: 'join.workingCopyHistory', label: localize('join.workingCopyHistory', "Saving local history") }); + } + + private onDidChangeModels(): void { + if (!this.storeAllScheduler.isScheduled()) { + this.storeAllScheduler.schedule(); + } + } + + private async storeAll(token: CancellationToken): Promise<void> { + const limiter = new Limiter(MAX_PARALLEL_HISTORY_IO_OPS); + const promises = []; + + const models = Array.from(this.models.values()); + for (const model of models) { + promises.push(limiter.queue(async () => { + if (token.isCancellationRequested) { + return; + } + + try { + await model.store(token); + } catch (error) { + this.logService.trace(error); + } + })); + } + + await Promise.all(promises); } } diff --git a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts index 157a36e5cdd..307ed8eb379 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts @@ -247,7 +247,7 @@ suite('FileWorkingCopyManager', () => { const workingCopy = await manager.resolve({ associatedResource: { path: '/some/associated.txt' } }); workingCopy.model?.updateContents('Simple Save As with associated resource'); - const target = URI.from({ scheme: Schemas.vscodeRemote, path: '/some/associated.txt' }); + const target = URI.from({ scheme: Schemas.file, path: '/some/associated.txt' }); accessor.fileService.notExistsSet.set(target, true); diff --git a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts index e37181dafe7..69f8c133efc 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts @@ -226,7 +226,7 @@ suite('UntitledFileWorkingCopyManager', () => { const workingCopy = await manager.untitled.resolve({ associatedResource: { path: '/some/associated.txt' } }); workingCopy.model?.updateContents('Simple Save with associated resource'); - accessor.fileService.notExistsSet.set(URI.from({ scheme: Schemas.vscodeRemote, path: '/some/associated.txt' }), true); + accessor.fileService.notExistsSet.set(URI.from({ scheme: Schemas.file, path: '/some/associated.txt' }), true); const result = await workingCopy.save(); assert.ok(result); diff --git a/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyHistoryService.test.ts b/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyHistoryService.test.ts index 55fbd774430..596255d0466 100644 --- a/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyHistoryService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyHistoryService.test.ts @@ -17,13 +17,12 @@ import { dirname, join } from 'vs/base/common/path'; import { Promises } from 'vs/base/node/pfs'; import { URI } from 'vs/base/common/uri'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import { TestRemoteAgentService } from 'vs/workbench/services/remote/test/common/testServices'; import { existsSync, readFileSync, unlinkSync } from 'fs'; import { IWorkingCopyHistoryEntry, IWorkingCopyHistoryEntryDescriptor, IWorkingCopyHistoryEvent } from 'vs/workbench/services/workingCopy/common/workingCopyHistory'; import { IFileService } from 'vs/platform/files/common/files'; import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; import { LabelService } from 'vs/workbench/services/label/common/labelService'; -import { TestLifecycleService, TestWillShutdownEvent } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestLifecycleService, TestRemoteAgentService, TestWillShutdownEvent } from 'vs/workbench/test/browser/workbenchTestServices'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { NativeWorkingCopyHistoryService } from 'vs/workbench/services/workingCopy/electron-sandbox/workingCopyHistoryService'; import { joinPath, dirname as resourcesDirname, basename } from 'vs/base/common/resources'; @@ -58,7 +57,7 @@ export class TestWorkingCopyHistoryService extends NativeWorkingCopyHistoryServi const uriIdentityService = new UriIdentityService(fileService); - const labelService = new LabelService(environmentService, new TestContextService(), new TestNativePathService()); + const labelService = new LabelService(environmentService, new TestContextService(), new TestNativePathService(), new TestRemoteAgentService()); const lifecycleService = new TestLifecycleService(); diff --git a/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts b/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts index 72f3a0b82ed..ebe492e36ed 100644 --- a/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts +++ b/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts @@ -14,7 +14,7 @@ import { ConfigurationScope, IConfigurationRegistry, Extensions as Configuration import { Registry } from 'vs/platform/registry/common/platform'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { distinct, firstOrDefault } from 'vs/base/common/arrays'; -import { basename, isEqual, isEqualAuthority, removeTrailingPathSeparator } from 'vs/base/common/resources'; +import { basename, isEqual, isEqualAuthority, joinPath, removeTrailingPathSeparator } from 'vs/base/common/resources'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { IFileService } from 'vs/platform/files/common/files'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -58,7 +58,7 @@ export abstract class AbstractWorkspaceEditingService implements IWorkspaceEditi saveLabel: mnemonicButtonLabel(localize('save', "Save")), title: localize('saveWorkspace', "Save Workspace"), filters: WORKSPACE_FILTER, - defaultUri: await this.fileDialogService.defaultWorkspacePath(undefined, this.getNewWorkspaceName()), + defaultUri: joinPath(await this.fileDialogService.defaultWorkspacePath(), this.getNewWorkspaceName()), availableFileSystems }); diff --git a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts index 38877625d85..e5039e9bc04 100644 --- a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts +++ b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts @@ -21,8 +21,7 @@ import { Memento, MementoObject } from 'vs/workbench/common/memento'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { isEqualAuthority } from 'vs/base/common/resources'; -import { ILogService } from 'vs/platform/log/common/log'; -import { isCI } from 'vs/base/common/platform'; +import { isWeb } from 'vs/base/common/platform'; export const WORKSPACE_TRUST_ENABLED = 'security.workspace.trust.enabled'; export const WORKSPACE_TRUST_STARTUP_PROMPT = 'security.workspace.trust.startupPrompt'; @@ -119,8 +118,7 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService, - @IWorkspaceTrustEnablementService private readonly workspaceTrustEnablementService: IWorkspaceTrustEnablementService, - @ILogService protected readonly _logService: ILogService, + @IWorkspaceTrustEnablementService private readonly workspaceTrustEnablementService: IWorkspaceTrustEnablementService ) { super(); @@ -134,7 +132,7 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork this._workspaceTrustInitializedPromiseResolve = resolve; }); - this._storedTrustState = new WorkspaceTrustMemento(this.storageService); + this._storedTrustState = new WorkspaceTrustMemento(isWeb && this.workspaceService.getWorkbenchState() === WorkbenchState.EMPTY ? undefined : this.storageService); this._trustTransitionManager = this._register(new WorkspaceTrustTransitionManager()); this._trustStateInfo = this.loadTrustInfo(); @@ -147,10 +145,6 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork //#region initialize private initializeWorkspaceTrust(): void { - if (isCI) { - this._logService.info(`[WT] Enter initializeWorkspaceTrust()...`); - } - // Resolve canonical Uris this.resolveCanonicalUris() .then(async () => { @@ -158,15 +152,9 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork await this.updateWorkspaceTrust(); }) .finally(() => { - if (isCI) { - this._logService.info(`[WT] Open workspaceResolved gate...`); - } this._workspaceResolvedPromiseResolve(); if (!this.environmentService.remoteAuthority) { - if (isCI) { - this._logService.info(`[WT] Open workspaceTrustInitialized gate...`); - } this._workspaceTrustInitializedPromiseResolve(); } }); @@ -211,33 +199,21 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork } private async getCanonicalUri(uri: URI): Promise<URI> { - if (isCI) { - this._logService.info('[WT] Enter getCanonicalUri()...'); - } - + let canonicalUri = uri; if (this.environmentService.remoteAuthority && uri.scheme === Schemas.vscodeRemote) { - if (isCI) { - this._logService.info('[WT] Return this.remoteAuthorityResolverService.getCanonicalURI(uri)...'); - } - - return this.remoteAuthorityResolverService.getCanonicalURI(uri); - } - - if (uri.scheme === 'vscode-vfs') { + canonicalUri = await this.remoteAuthorityResolverService.getCanonicalURI(uri); + } else if (uri.scheme === 'vscode-vfs') { const index = uri.authority.indexOf('+'); if (index !== -1) { - return uri.with({ authority: uri.authority.substr(0, index) }); + canonicalUri = uri.with({ authority: uri.authority.substr(0, index) }); } } - return uri; + // ignore query and fragent section of uris always + return canonicalUri.with({ query: null, fragment: null }); } private async resolveCanonicalUris(): Promise<void> { - if (isCI) { - this._logService.info('[WT] Enter resolveCanonicalUris()...'); - } - // Open editors const filesToOpen: IPath[] = []; if (this.environmentService.filesToOpenOrCreate) { @@ -249,38 +225,22 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork } if (filesToOpen.length) { - const filesToOpenOrCreateUris = filesToOpen.filter(f => f.fileUri && f.fileUri.scheme === Schemas.file).map(f => f.fileUri!); + const filesToOpenOrCreateUris = filesToOpen.filter(f => !!f.fileUri).map(f => f.fileUri!); const canonicalFilesToOpen = await Promise.all(filesToOpenOrCreateUris.map(uri => this.getCanonicalUri(uri))); this._canonicalStartupFiles.push(...canonicalFilesToOpen.filter(uri => this._canonicalStartupFiles.every(u => !this.uriIdentityService.extUri.isEqual(uri, u)))); } - if (isCI) { - this._logService.info('[WT] Done processing open editors...'); - } - // Workspace const workspaceUris = this.workspaceService.getWorkspace().folders.map(f => f.uri); const canonicalWorkspaceFolders = await Promise.all(workspaceUris.map(uri => this.getCanonicalUri(uri))); - if (isCI) { - this._logService.info('[WT] Done processing workspace folders...'); - } - let canonicalWorkspaceConfiguration = this.workspaceService.getWorkspace().configuration; if (canonicalWorkspaceConfiguration && isSavedWorkspace(canonicalWorkspaceConfiguration, this.environmentService)) { canonicalWorkspaceConfiguration = await this.getCanonicalUri(canonicalWorkspaceConfiguration); } - if (isCI) { - this._logService.info('[WT] Done processing workspace configuration...'); - } - this._canonicalWorkspace = new CanonicalWorkspace(this.workspaceService.getWorkspace(), canonicalWorkspaceFolders, canonicalWorkspaceConfiguration); - - if (isCI) { - this._logService.info('[WT] Exit resolveCanonicalUris()...'); - } } private loadTrustInfo(): IWorkspaceTrustInfo { @@ -362,16 +322,7 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork } private async updateWorkspaceTrust(trusted?: boolean): Promise<void> { - if (isCI) { - this._logService.info(`[WT] Enter updateWorkspaceTrust()...`); - } - if (!this.workspaceTrustEnablementService.isWorkspaceTrustEnabled()) { - if (isCI) { - this._logService.info(`[WT] Workspace trust is disabled.`); - this._logService.info(`[WT] Exit updateWorkspaceTrust()...`); - } - return; } @@ -883,15 +834,19 @@ class WorkspaceTrustTransitionManager extends Disposable { class WorkspaceTrustMemento { - private readonly _memento: Memento; + private readonly _memento?: Memento; private readonly _mementoObject: MementoObject; private readonly _acceptsOutOfWorkspaceFilesKey = 'acceptsOutOfWorkspaceFiles'; private readonly _isEmptyWorkspaceTrustedKey = 'isEmptyWorkspaceTrusted'; - constructor(storageService: IStorageService) { - this._memento = new Memento('workspaceTrust', storageService); - this._mementoObject = this._memento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE); + constructor(storageService?: IStorageService) { + if (storageService) { + this._memento = new Memento('workspaceTrust', storageService); + this._mementoObject = this._memento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE); + } else { + this._mementoObject = {}; + } } get acceptsOutOfWorkspaceFiles(): boolean { @@ -900,7 +855,10 @@ class WorkspaceTrustMemento { set acceptsOutOfWorkspaceFiles(value: boolean) { this._mementoObject[this._acceptsOutOfWorkspaceFilesKey] = value; - this._memento.saveMemento(); + + if (this._memento) { + this._memento.saveMemento(); + } } get isEmptyWorkspaceTrusted(): boolean | undefined { @@ -909,7 +867,10 @@ class WorkspaceTrustMemento { set isEmptyWorkspaceTrusted(value: boolean | undefined) { this._mementoObject[this._isEmptyWorkspaceTrustedKey] = value; - this._memento.saveMemento(); + + if (this._memento) { + this._memento.saveMemento(); + } } } diff --git a/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts b/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts index da388396897..cb8808efc50 100644 --- a/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts +++ b/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts @@ -10,7 +10,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { FileService } from 'vs/platform/files/common/fileService'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; -import { ILogService, NullLogService } from 'vs/platform/log/common/log'; +import { NullLogService } from 'vs/platform/log/common/log'; import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; @@ -28,7 +28,6 @@ suite('Workspace Trust', () => { let instantiationService: TestInstantiationService; let configurationService: TestConfigurationService; let environmentService: IWorkbenchEnvironmentService; - let logService: ILogService; setup(async () => { instantiationService = new TestInstantiationService(); @@ -39,10 +38,7 @@ suite('Workspace Trust', () => { environmentService = {} as IWorkbenchEnvironmentService; instantiationService.stub(IWorkbenchEnvironmentService, environmentService); - logService = new NullLogService(); - instantiationService.stub(ILogService, logService); - - instantiationService.stub(IUriIdentityService, new UriIdentityService(new FileService(logService))); + instantiationService.stub(IUriIdentityService, new UriIdentityService(new FileService(new NullLogService()))); instantiationService.stub(IRemoteAuthorityResolverService, new class extends mock<IRemoteAuthorityResolverService>() { }); }); diff --git a/src/vs/workbench/test/browser/parts/editor/editorGroupModel.test.ts b/src/vs/workbench/test/browser/parts/editor/editorGroupModel.test.ts index 4023aef7563..3f4659894a3 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorGroupModel.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorGroupModel.test.ts @@ -2232,6 +2232,40 @@ suite('EditorGroupModel', () => { assert.strictEqual(group.indexOf(input4), 2); }); + test('Sticky/Unsticky Editors sends correct editor index', function () { + const group = createEditorGroupModel(); + + const input1 = input(); + const input2 = input(); + const input3 = input(); + + group.openEditor(input1, { pinned: true, active: true }); + group.openEditor(input2, { pinned: true, active: true }); + group.openEditor(input3, { pinned: false, active: true }); + + assert.strictEqual(group.stickyCount, 0); + + const events = groupListener(group); + + group.stick(input3); + + assert.strictEqual(events.sticky[0].editorIndex, 0); + assert.strictEqual(group.isSticky(input3), true); + assert.strictEqual(group.stickyCount, 1); + + group.stick(input2); + + assert.strictEqual(events.sticky[1].editorIndex, 1); + assert.strictEqual(group.isSticky(input2), true); + assert.strictEqual(group.stickyCount, 2); + + group.unstick(input3); + assert.strictEqual(events.unsticky[0].editorIndex, 1); + assert.strictEqual(group.isSticky(input3), false); + assert.strictEqual(group.isSticky(input2), true); + assert.strictEqual(group.stickyCount, 1); + }); + test('onDidMoveEditor Event', () => { const group1 = createEditorGroupModel(); const group2 = createEditorGroupModel(); diff --git a/src/vs/workbench/test/browser/parts/editor/editorPane.test.ts b/src/vs/workbench/test/browser/parts/editor/editorPane.test.ts index dbb629c311f..7d19418acc5 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorPane.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorPane.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { EditorPane, EditorMemento } from 'vs/workbench/browser/parts/editor/editorPane'; -import { WorkspaceTrustRequiredEditor } from 'vs/workbench/browser/parts/editor/editorPlaceholder'; +import { WorkspaceTrustRequiredPlaceholderEditor } from 'vs/workbench/browser/parts/editor/editorPlaceholder'; import { IEditorSerializer, IEditorFactoryRegistry, EditorExtensions, EditorInputCapabilities, IEditorDescriptor, IEditorPane } from 'vs/workbench/common/editor'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -505,7 +505,7 @@ suite('EditorPane', () => { const testInput = new TrustRequiredTestInput(); await group.openEditor(testInput); - assert.strictEqual(group.activeEditorPane?.getId(), WorkspaceTrustRequiredEditor.ID); + assert.strictEqual(group.activeEditorPane?.getId(), WorkspaceTrustRequiredPlaceholderEditor.ID); const getEditorPaneIdAsync = () => new Promise(resolve => { disposables.add(editorService.onDidActiveEditorChange(() => { @@ -518,7 +518,7 @@ suite('EditorPane', () => { assert.strictEqual(await getEditorPaneIdAsync(), 'trustRequiredTestEditor'); workspaceTrustService.setWorkspaceTrust(false); - assert.strictEqual(await getEditorPaneIdAsync(), WorkspaceTrustRequiredEditor.ID); + assert.strictEqual(await getEditorPaneIdAsync(), WorkspaceTrustRequiredPlaceholderEditor.ID); dispose(disposables); }); diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index fbd98039411..62e691e60bb 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -7,7 +7,7 @@ import { FileEditorInput } from 'vs/workbench/contrib/files/browser/editors/file import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { basename, isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { ITelemetryData, ITelemetryService, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { EditorInputWithOptions, IEditorIdentifier, IUntitledTextResourceEditorInput, IResourceDiffEditorInput, IEditorPane, IEditorCloseEvent, IEditorPartOptions, IRevertOptions, GroupIdentifier, EditorsOrder, IFileEditorInput, IEditorFactoryRegistry, IEditorSerializer, EditorExtensions, ISaveOptions, IMoveResult, ITextDiffEditorPane, IVisibleEditorPane, IEditorOpenContext, EditorExtensions as Extensions, EditorInputCapabilities, IUntypedEditorInput, IEditorWillMoveEvent, IEditorWillOpenEvent, IActiveEditorChangeEvent, EditorPaneSelectionChangeReason, IEditorPaneSelection } from 'vs/workbench/common/editor'; @@ -21,7 +21,7 @@ import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { IEditorOptions, IResourceEditorInput, IEditorModel, IResourceEditorInputIdentifier, ITextResourceEditorInput, ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { IUntitledTextEditorService, UntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; import { IWorkspaceContextService, IWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; -import { ILifecycleService, ShutdownReason, StartupKind, LifecyclePhase, WillShutdownEvent, BeforeShutdownErrorEvent, InternalBeforeShutdownEvent } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { ILifecycleService, ShutdownReason, StartupKind, LifecyclePhase, WillShutdownEvent, BeforeShutdownErrorEvent, InternalBeforeShutdownEvent, IWillShutdownEventJoiner } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { FileOperationEvent, IFileService, IFileStat, IFileStatResult, FileChangesEvent, IResolveFileOptions, ICreateFileOptions, IFileSystemProvider, FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileType, IFileDeleteOptions, IFileOverwriteOptions, IFileWriteOptions, IFileOpenOptions, IFileStatWithMetadata, IResolveMetadataFileOptions, IWriteFileOptions, IReadFileOptions, IFileContent, IFileStreamContent, FileOperationError, IFileSystemProviderWithFileReadStreamCapability, IFileReadStreamOptions, IReadFileStreamOptions, IFileSystemProviderCapabilitiesChangeEvent, IFileStatWithPartialMetadata } from 'vs/platform/files/common/files'; import { IModelService } from 'vs/editor/common/services/model'; @@ -153,6 +153,12 @@ import { TextEditorPaneSelection } from 'vs/workbench/browser/parts/editor/textE import { Selection } from 'vs/editor/common/core/selection'; import { IFolderBackupInfo, IWorkspaceBackupInfo } from 'vs/platform/backup/common/backup'; import { TestEditorWorkerService } from 'vs/editor/test/common/services/testEditorWorkerService'; +import { IExtensionHostExitInfo, IRemoteAgentConnection, IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { ILanguageDetectionService } from 'vs/workbench/services/languageDetection/common/languageDetectionWorkerService'; +import { IDiagnosticInfoOptions, IDiagnosticInfo } from 'vs/platform/diagnostics/common/diagnostics'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ISocketFactory } from 'vs/platform/remote/common/remoteAgentConnection'; +import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment'; export function createFileEditorInput(instantiationService: IInstantiationService, resource: URI): FileEditorInput { return instantiationService.createInstance(FileEditorInput, resource, undefined, undefined, undefined, undefined, undefined, undefined); @@ -252,6 +258,8 @@ export function workbenchInstantiationService( instantiationService.stub(ITextResourceConfigurationService, new TestTextResourceConfigurationService(configService)); instantiationService.stub(IUntitledTextEditorService, disposables.add(instantiationService.createInstance(UntitledTextEditorService))); instantiationService.stub(IStorageService, disposables.add(new TestStorageService())); + instantiationService.stub(IRemoteAgentService, new TestRemoteAgentService()); + instantiationService.stub(ILanguageDetectionService, new TestLanguageDetectionService()); instantiationService.stub(IPathService, overrides?.pathService ? overrides.pathService(instantiationService) : new TestPathService()); const layoutService = new TestLayoutService(); instantiationService.stub(IWorkbenchLayoutService, layoutService); @@ -865,7 +873,7 @@ export class TestEditorGroupView implements IEditorGroupView { copyEditors(_editors: EditorInputWithOptions[], _target: IEditorGroup): void { } async closeEditor(_editor?: EditorInput, options?: ICloseEditorOptions): Promise<boolean> { return true; } async closeEditors(_editors: EditorInput[] | ICloseEditorsFilter, options?: ICloseEditorOptions): Promise<boolean> { return true; } - async closeAllEditors(options?: ICloseAllEditorsOptions): Promise<void> { } + async closeAllEditors(options?: ICloseAllEditorsOptions): Promise<boolean> { return true; } async replaceEditors(_editors: IEditorReplacement[]): Promise<void> { } pinEditor(_editor?: EditorInput): void { } stickEditor(editor?: EditorInput | undefined): void { } @@ -1227,6 +1235,7 @@ export class TestLifecycleService implements ILifecycleService { join: p => { this.shutdownJoiners.push(p); }, + joiners: () => [], force: () => { /* No-Op in tests */ }, token: CancellationToken.None, reason @@ -1261,10 +1270,11 @@ export class TestBeforeShutdownEvent implements InternalBeforeShutdownEvent { export class TestWillShutdownEvent implements WillShutdownEvent { value: Promise<void>[] = []; + joiners = () => []; reason = ShutdownReason.CLOSE; token = CancellationToken.None; - join(promise: Promise<void>, id: string): void { + join(promise: Promise<void>, joiner: IWillShutdownEventJoiner): void { this.value.push(promise); } @@ -1675,7 +1685,7 @@ export class TestPathService implements IPathService { declare readonly _serviceBrand: undefined; - constructor(private readonly fallbackUserHome: URI = URI.from({ scheme: Schemas.vscodeRemote, path: '/' }), public defaultUriScheme = Schemas.vscodeRemote) { } + constructor(private readonly fallbackUserHome: URI = URI.from({ scheme: Schemas.file, path: '/' }), public defaultUriScheme = Schemas.file) { } hasValidBasename(resource: URI, basename?: string): Promise<boolean>; hasValidBasename(resource: URI, os: OperatingSystem, basename?: string): boolean; @@ -1689,7 +1699,12 @@ export class TestPathService implements IPathService { get path() { return Promise.resolve(isWindows ? win32 : posix); } - async userHome() { return this.fallbackUserHome; } + userHome(options?: { preferLocal: boolean }): Promise<URI>; + userHome(options: { preferLocal: true }): URI; + userHome(options?: { preferLocal: boolean }): Promise<URI> | URI { + return options?.preferLocal ? this.fallbackUserHome : Promise.resolve(this.fallbackUserHome); + } + get resolvedUserHome() { return this.fallbackUserHome; } async fileURI(path: string): Promise<URI> { @@ -1874,3 +1889,32 @@ export class TestQuickInputService implements IQuickInputService { back(): Promise<void> { throw new Error('not implemented.'); } cancel(): Promise<void> { throw new Error('not implemented.'); } } + +class TestLanguageDetectionService implements ILanguageDetectionService { + + declare readonly _serviceBrand: undefined; + + isEnabledForLanguage(languageId: string): boolean { return false; } + async detectLanguage(resource: URI, supportedLangs?: string[] | undefined): Promise<string | undefined> { return undefined; } +} + +export class TestRemoteAgentService implements IRemoteAgentService { + + declare readonly _serviceBrand: undefined; + + socketFactory: ISocketFactory = { + connect() { } + }; + + getConnection(): IRemoteAgentConnection | null { return null; } + async getEnvironment(): Promise<IRemoteAgentEnvironment | null> { return null; } + async getRawEnvironment(): Promise<IRemoteAgentEnvironment | null> { return null; } + async getExtensionHostExitInfo(reconnectionToken: string): Promise<IExtensionHostExitInfo | null> { return null; } + async whenExtensionsReady(): Promise<void> { } + scanExtensions(skipExtensions?: ExtensionIdentifier[]): Promise<IExtensionDescription[]> { throw new Error('Method not implemented.'); } + scanSingleExtension(extensionLocation: URI, isBuiltin: boolean): Promise<IExtensionDescription | null> { throw new Error('Method not implemented.'); } + async getDiagnosticInfo(options: IDiagnosticInfoOptions): Promise<IDiagnosticInfo | undefined> { return undefined; } + async updateTelemetryLevel(telemetryLevel: TelemetryLevel): Promise<void> { } + async logTelemetry(eventName: string, data?: ITelemetryData): Promise<void> { } + async flushTelemetry(): Promise<void> { } +} diff --git a/src/vs/workbench/test/common/notifications.test.ts b/src/vs/workbench/test/common/notifications.test.ts index dbeb93bfad0..1bdef91760a 100644 --- a/src/vs/workbench/test/common/notifications.test.ts +++ b/src/vs/workbench/test/common/notifications.test.ts @@ -127,7 +127,7 @@ suite('Notifications', () => { assert.strictEqual(called, 1); // Error with Action - let item7 = NotificationViewItem.create({ severity: Severity.Error, message: createErrorWithActions('Hello Error', { actions: [new Action('id', 'label')] }) })!; + let item7 = NotificationViewItem.create({ severity: Severity.Error, message: createErrorWithActions('Hello Error', [new Action('id', 'label')]) })!; assert.strictEqual(item7.actions!.primary!.length, 1); // Filter diff --git a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts index ba5b81b1d97..9b1228a4c69 100644 --- a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts @@ -203,6 +203,7 @@ export class TestNativeHostService implements INativeHostService { async maximizeWindow(): Promise<void> { } async unmaximizeWindow(): Promise<void> { } async minimizeWindow(): Promise<void> { } + async updateTitleBarOverlay(backgroundColor: string, foregroundColor: string): Promise<void> { } async setMinimumSize(width: number | undefined, height: number | undefined): Promise<void> { } async saveWindowSplash(value: IPartsSplash): Promise<void> { } async focusWindow(options?: { windowId?: number | undefined } | undefined): Promise<void> { } @@ -245,6 +246,7 @@ export class TestNativeHostService implements INativeHostService { async toggleDevTools(): Promise<void> { } async toggleSharedProcessWindow(): Promise<void> { } async resolveProxy(url: string): Promise<string | undefined> { return undefined; } + async findFreePort(startPort: number, giveUpAfter: number, timeout: number, stride?: number): Promise<number> { return -1; } async readClipboardText(type?: 'selection' | 'clipboard' | undefined): Promise<string> { return ''; } async writeClipboardText(text: string, type?: 'selection' | 'clipboard' | undefined): Promise<void> { } async readClipboardFindText(): Promise<string> { return ''; } diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index dfc2bcadef0..bf87db19d53 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -83,6 +83,7 @@ import 'vs/workbench/services/extensionRecommendations/common/extensionIgnoredRe import 'vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig'; import 'vs/workbench/services/notification/common/notificationService'; import 'vs/workbench/services/userDataSync/common/userDataSyncUtil'; +import 'vs/workbench/services/profiles/common/profileService'; import 'vs/workbench/services/remote/common/remoteExplorerService'; import 'vs/workbench/services/workingCopy/common/workingCopyService'; import 'vs/workbench/services/workingCopy/common/workingCopyFileService'; @@ -303,6 +304,9 @@ import 'vs/workbench/contrib/typeHierarchy/browser/typeHierarchy.contribution'; import 'vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline'; import 'vs/workbench/contrib/outline/browser/outline.contribution'; +// Language Detection +import 'vs/workbench/contrib/languageDetection/browser/languageDetection.contribution'; + // Language Status import 'vs/workbench/contrib/languageStatus/browser/languageStatus.contribution'; @@ -315,6 +319,9 @@ import 'vs/workbench/contrib/feedback/browser/feedback.contribution'; // User Data Sync import 'vs/workbench/contrib/userDataSync/browser/userDataSync.contribution'; +// Profiles +import 'vs/workbench/contrib/profiles/common/profiles.contribution'; + // Code Actions import 'vs/workbench/contrib/codeActions/browser/codeActions.contribution'; @@ -336,4 +343,7 @@ import 'vs/workbench/contrib/list/browser/list.contribution'; // Audio Cues import 'vs/workbench/contrib/audioCues/browser/audioCues.contribution'; +// Drop into editor +import 'vs/workbench/contrib/dropIntoEditor/browser/dropIntoEditor.contibution'; + //#endregion diff --git a/src/vs/workbench/workbench.sandbox.main.ts b/src/vs/workbench/workbench.sandbox.main.ts index 87f933f5fe0..e0708193c34 100644 --- a/src/vs/workbench/workbench.sandbox.main.ts +++ b/src/vs/workbench/workbench.sandbox.main.ts @@ -61,6 +61,7 @@ import 'vs/workbench/services/encryption/electron-sandbox/encryptionService'; import 'vs/workbench/services/localizations/electron-sandbox/localizationsService'; import 'vs/workbench/services/telemetry/electron-sandbox/telemetryService'; import 'vs/workbench/services/extensions/electron-sandbox/extensionHostStarter'; +import 'vs/platform/extensionManagement/electron-sandbox/extensionsScannerService'; import 'vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementServerService'; import 'vs/workbench/services/extensionManagement/electron-sandbox/extensionTipsService'; import 'vs/workbench/services/userDataSync/electron-sandbox/userDataSyncMachinesService'; diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts index 8f3d9020fd0..925c0010a0b 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -61,6 +61,7 @@ import 'vs/workbench/services/tunnel/browser/tunnelService'; import 'vs/workbench/services/files/browser/elevatedFileService'; import 'vs/workbench/services/workingCopy/browser/workingCopyHistoryService'; import 'vs/workbench/services/userDataSync/browser/webUserDataSyncEnablementService'; +import 'vs/workbench/services/configurationResolver/browser/configurationResolverService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; @@ -85,8 +86,6 @@ import { NullEndpointTelemetryService } from 'vs/platform/telemetry/common/telem import { ITitleService } from 'vs/workbench/services/title/common/titleService'; import { TitlebarPart } from 'vs/workbench/browser/parts/titlebar/titlebarPart'; import { ITimerService, TimerService } from 'vs/workbench/services/timer/browser/timerService'; -import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; -import { ConfigurationResolverService } from 'vs/workbench/services/configurationResolver/browser/configurationResolverService'; import { IDiagnosticsService, NullDiagnosticsService } from 'vs/platform/diagnostics/common/diagnostics'; registerSingleton(IWorkbenchExtensionManagementService, ExtensionManagementService); @@ -102,7 +101,6 @@ registerSingleton(IUserDataAutoSyncService, UserDataAutoSyncService); registerSingleton(ITitleService, TitlebarPart); registerSingleton(IExtensionTipsService, ExtensionTipsService); registerSingleton(ITimerService, TimerService); -registerSingleton(IConfigurationResolverService, ConfigurationResolverService, true); registerSingleton(ICustomEndpointTelemetryService, NullEndpointTelemetryService, true); registerSingleton(IDiagnosticsService, NullDiagnosticsService, true); @@ -114,6 +112,9 @@ registerSingleton(IDiagnosticsService, NullDiagnosticsService, true); // Output import 'vs/workbench/contrib/output/common/outputChannelModelService'; +// Logs +import 'vs/workbench/contrib/logs/browser/logs.contribution'; + // Explorer import 'vs/workbench/contrib/files/browser/files.web.contribution'; @@ -168,7 +169,7 @@ import 'vs/workbench/contrib/offline/browser/offline.contribution'; // // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -import { create, commands, env } from 'vs/workbench/browser/web.factory'; +import { create, commands, env, window } 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'; @@ -183,7 +184,6 @@ import type { IUpdateProvider, IUpdate } from 'vs/workbench/services/update/brow // eslint-disable-next-line no-duplicate-imports import type { IWorkspace, IWorkspaceProvider } from 'vs/workbench/services/host/browser/browserHostService'; - export { // Factory @@ -250,6 +250,9 @@ export { commands, Menu, + // Window + window, + // Branding IHomeIndicator, IWelcomeBanner, |