diff --git a/extensions/markdown-math/package.json b/extensions/markdown-math/package.json index 33eea8a6dad..7d0b42cddc2 100644 --- a/extensions/markdown-math/package.json +++ b/extensions/markdown-math/package.json @@ -74,7 +74,7 @@ ], "configuration": [ { - "title": "Markdown", + "title": "Markdown Math", "properties": { "markdown.math.enabled": { "type": "boolean", diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts index be513511aa8..b2271faeea6 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts @@ -233,7 +233,7 @@ import { assertNoRpc } from '../utils'; deepStrictEqual(terminal.state, { interactedWith: false }); const eventState = await new Promise(r => { disposables.push(window.onDidChangeTerminalState(e => { - if (e.terminal === terminal) { + if (e === terminal) { r(e.state); } })); diff --git a/package.json b/package.json index 1374bb0af67..b762ad9c67f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.60.0", - "distro": "0d8a917f4624b966e7031ad64f2f7504926ae67d", + "distro": "62b8d83a4b9a7ad55e7356375f40ec7875f4c209", "author": { "name": "Microsoft Corporation" }, @@ -84,12 +84,12 @@ "vscode-regexpp": "^3.1.0", "vscode-ripgrep": "^1.12.0", "vscode-textmate": "5.4.0", - "xterm": "4.14.0-beta.18", + "xterm": "4.14.0-beta.21", "xterm-addon-search": "0.9.0-beta.4", "xterm-addon-serialize": "0.6.0-beta.7", "xterm-addon-unicode11": "0.3.0-beta.6", - "xterm-addon-webgl": "0.12.0-beta.8", - "xterm-headless": "4.14.0-beta.8", + "xterm-addon-webgl": "0.12.0-beta.10", + "xterm-headless": "4.14.0-beta.11", "yauzl": "^2.9.2", "yazl": "^2.4.3" }, diff --git a/remote/package.json b/remote/package.json index 61c09bd1ea5..c7889488518 100644 --- a/remote/package.json +++ b/remote/package.json @@ -24,12 +24,12 @@ "vscode-regexpp": "^3.1.0", "vscode-ripgrep": "^1.12.0", "vscode-textmate": "5.4.0", - "xterm": "4.14.0-beta.18", + "xterm": "4.14.0-beta.21", "xterm-addon-search": "0.9.0-beta.4", "xterm-addon-serialize": "0.6.0-beta.7", "xterm-addon-unicode11": "0.3.0-beta.6", - "xterm-addon-webgl": "0.12.0-beta.8", - "xterm-headless": "4.14.0-beta.8", + "xterm-addon-webgl": "0.12.0-beta.10", + "xterm-headless": "4.14.0-beta.11", "yauzl": "^2.9.2", "yazl": "^2.4.3" }, diff --git a/remote/web/package.json b/remote/web/package.json index 20f224edc71..c0d845a989c 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -10,9 +10,9 @@ "tas-client-umd": "0.1.4", "vscode-oniguruma": "1.5.1", "vscode-textmate": "5.4.0", - "xterm": "4.14.0-beta.18", + "xterm": "4.14.0-beta.21", "xterm-addon-search": "0.9.0-beta.4", "xterm-addon-unicode11": "0.3.0-beta.6", - "xterm-addon-webgl": "0.12.0-beta.8" + "xterm-addon-webgl": "0.12.0-beta.10" } } diff --git a/remote/web/yarn.lock b/remote/web/yarn.lock index 96339b5e989..d5534b32e03 100644 --- a/remote/web/yarn.lock +++ b/remote/web/yarn.lock @@ -123,12 +123,12 @@ xterm-addon-unicode11@0.3.0-beta.6: resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.3.0-beta.6.tgz#8914f377757d5078e7b4daee7d3e2b7428b6edf0" integrity sha512-Qwa18yMhtacf9Jtxy+UuxHfjIeIjaX9q0LUfHtZU8/Lwjh+bGcn8E8IABVSGvXZgPNKw/4TqEpgLFexn+sfc5g== -xterm-addon-webgl@0.12.0-beta.8: - version "0.12.0-beta.8" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.12.0-beta.8.tgz#c11cc8f9065986efe48422b2bfc8427403f30d5f" - integrity sha512-T6qQlol20ezuTdEC+iwuxNFZfCej7thBqKGA5mTBXSDpXoWUw//h/f8iFU0lqNNAI0zgqIPulIiRRJRTUVM8pA== +xterm-addon-webgl@0.12.0-beta.10: + version "0.12.0-beta.10" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.12.0-beta.10.tgz#ba23287043da8172f4f9e53babb620f54ad36189" + integrity sha512-mzMOAqgM95FAgzcVzCH/Q0NfN0CTMHVDWCCFyg4B5ZcsuRiQKqQQw0HS+5uOQDtoZEDl2BqGFby7pGpENWGjZQ== -xterm@4.14.0-beta.18: - version "4.14.0-beta.18" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.14.0-beta.18.tgz#57a7cbf5544b6e42c7a37d016b1adf37e679e9a3" - integrity sha512-Vyr3G6C+vPEn1MLfDEkZfOhwmG5byiVM1eQ0rf+yLg8a1rz3Oa4ji4IkA6SGr3ztqQ5ZQA6Q/10Rop+1dk7PrA== +xterm@4.14.0-beta.21: + version "4.14.0-beta.21" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.14.0-beta.21.tgz#2d50328389cc79021c0202405689955fc18cb703" + integrity sha512-9ELD78FTUL91OBRfNVWh+gxEqufNNWsrFkkOFxhKBSk3YRuJdcapZBb6afobgpAaQglw8v8Ze1eBkTtctW20jQ== diff --git a/remote/yarn.lock b/remote/yarn.lock index e315ff7bfed..f0c0ca912f4 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -635,20 +635,20 @@ xterm-addon-unicode11@0.3.0-beta.6: resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.3.0-beta.6.tgz#8914f377757d5078e7b4daee7d3e2b7428b6edf0" integrity sha512-Qwa18yMhtacf9Jtxy+UuxHfjIeIjaX9q0LUfHtZU8/Lwjh+bGcn8E8IABVSGvXZgPNKw/4TqEpgLFexn+sfc5g== -xterm-addon-webgl@0.12.0-beta.8: - version "0.12.0-beta.8" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.12.0-beta.8.tgz#c11cc8f9065986efe48422b2bfc8427403f30d5f" - integrity sha512-T6qQlol20ezuTdEC+iwuxNFZfCej7thBqKGA5mTBXSDpXoWUw//h/f8iFU0lqNNAI0zgqIPulIiRRJRTUVM8pA== +xterm-addon-webgl@0.12.0-beta.10: + version "0.12.0-beta.10" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.12.0-beta.10.tgz#ba23287043da8172f4f9e53babb620f54ad36189" + integrity sha512-mzMOAqgM95FAgzcVzCH/Q0NfN0CTMHVDWCCFyg4B5ZcsuRiQKqQQw0HS+5uOQDtoZEDl2BqGFby7pGpENWGjZQ== -xterm-headless@4.14.0-beta.8: - version "4.14.0-beta.8" - resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-4.14.0-beta.8.tgz#5591d438cb4940f8cef9a65a4cfc385ad09b9aa5" - integrity sha512-H/vZiuH7cwgGcbxIHCexFVHfCzFX8z0/yaxuDGK/G8G1DoqI3MVNFQPdugu/lkptyddD0O5Hn0dUV7hSCfxi+A== +xterm-headless@4.14.0-beta.11: + version "4.14.0-beta.11" + resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-4.14.0-beta.11.tgz#c97052e31ab07a50c577cdcf05878e4cff76deec" + integrity sha512-EL3cK0yXvQ9BDYqcAMXGd2NkHFFknYQZ7sWgVq6xWrMcSrOMGfIpNyZ1zlP4V5pUk0+yur52TS4xumJ+fYld5w== -xterm@4.14.0-beta.18: - version "4.14.0-beta.18" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.14.0-beta.18.tgz#57a7cbf5544b6e42c7a37d016b1adf37e679e9a3" - integrity sha512-Vyr3G6C+vPEn1MLfDEkZfOhwmG5byiVM1eQ0rf+yLg8a1rz3Oa4ji4IkA6SGr3ztqQ5ZQA6Q/10Rop+1dk7PrA== +xterm@4.14.0-beta.21: + version "4.14.0-beta.21" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.14.0-beta.21.tgz#2d50328389cc79021c0202405689955fc18cb703" + integrity sha512-9ELD78FTUL91OBRfNVWh+gxEqufNNWsrFkkOFxhKBSk3YRuJdcapZBb6afobgpAaQglw8v8Ze1eBkTtctW20jQ== yauzl@^2.9.2: version "2.10.0" diff --git a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf index 90ee0645a47..5fb9d7440bf 100644 Binary files a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf and b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf differ diff --git a/src/vs/base/common/codicons.ts b/src/vs/base/common/codicons.ts index 975842ab588..702f16ab09f 100644 --- a/src/vs/base/common/codicons.ts +++ b/src/vs/base/common/codicons.ts @@ -586,6 +586,8 @@ export namespace Codicon { export const gitPullRequestDraft = new Codicon('git-pull-request-draft', { fontCharacter: '\\ebdb' }); export const debugAll = new Codicon('debug-all', { fontCharacter: '\\ebdc' }); export const debugCoverage = new Codicon('debug-coverage', { fontCharacter: '\\ebdd' }); + export const runErrors = new Codicon('run-errors', { fontCharacter: '\\ebde' }); + export const folderLibrary = new Codicon('folder-library', { fontCharacter: '\\ebdf' }); export const dropDownButton = new Codicon('drop-down-button', Codicon.chevronDown.definition); } diff --git a/src/vs/base/common/fuzzyScorer.ts b/src/vs/base/common/fuzzyScorer.ts index a7ab501ab63..5b9648580c2 100644 --- a/src/vs/base/common/fuzzyScorer.ts +++ b/src/vs/base/common/fuzzyScorer.ts @@ -392,9 +392,9 @@ export function scoreItemFuzzy(item: T, query: IPreparedQuery, fuzzy: boolean // - whether fuzzy matching is enabled or not let cacheHash: string; if (description) { - cacheHash = `${label}${description}${query.normalized}${Array.isArray(query.values) ? query.values.length : ''}${fuzzy}`; + cacheHash = `${label}${description}${query.normalized}${Array.isArray(query.values) ? query.values.length : ''}${fuzzy}${query.expectExactMatch}`; } else { - cacheHash = `${label}${query.normalized}${Array.isArray(query.values) ? query.values.length : ''}${fuzzy}`; + cacheHash = `${label}${query.normalized}${Array.isArray(query.values) ? query.values.length : ''}${fuzzy}${query.expectExactMatch}`; } const cached = cache[cacheHash]; @@ -461,7 +461,11 @@ function doScoreItemFuzzySingle(label: string, description: string | undefined, // Prefer label matches if told so or we have no description if (preferLabelMatches || !description) { - const [labelScore, labelPositions] = scoreFuzzy(label, query.normalized, query.normalizedLowercase, fuzzy); + const [labelScore, labelPositions] = scoreFuzzy( + label, + query.normalized, + query.normalizedLowercase, + fuzzy && !query.expectExactMatch); if (labelScore) { // If we have a prefix match on the label, we give a much @@ -499,7 +503,11 @@ function doScoreItemFuzzySingle(label: string, description: string | undefined, const descriptionPrefixLength = descriptionPrefix.length; const descriptionAndLabel = `${descriptionPrefix}${label}`; - const [labelDescriptionScore, labelDescriptionPositions] = scoreFuzzy(descriptionAndLabel, query.normalized, query.normalizedLowercase, fuzzy); + const [labelDescriptionScore, labelDescriptionPositions] = scoreFuzzy( + descriptionAndLabel, + query.normalized, + query.normalizedLowercase, + fuzzy && !query.expectExactMatch); if (labelDescriptionScore) { const labelDescriptionMatches = createMatches(labelDescriptionPositions); const labelMatch: IMatch[] = []; @@ -793,6 +801,13 @@ export interface IPreparedQueryPiece { */ normalized: string; normalizedLowercase: string; + + /** + * The query is wrapped in quotes which means + * this query must be a substring of the input. + * In other words, no fuzzy matching is used. + */ + expectExactMatch: boolean; } export interface IPreparedQuery extends IPreparedQueryPiece { @@ -808,6 +823,14 @@ export interface IPreparedQuery extends IPreparedQueryPiece { containsPathSeparator: boolean; } +/* + * If a query is wrapped in quotes, the user does not want to + * use fuzzy search for this query. + */ +function queryExpectsExactMatch(query: string) { + return query.startsWith('"') && query.endsWith('"'); +} + /** * Helper function to prepare a search value for scoring by removing unwanted characters * and allowing to score on multiple pieces separated by whitespace character. @@ -821,12 +844,14 @@ export function prepareQuery(original: string): IPreparedQuery { const originalLowercase = original.toLowerCase(); const { pathNormalized, normalized, normalizedLowercase } = normalizeQuery(original); const containsPathSeparator = pathNormalized.indexOf(sep) >= 0; + const expectExactMatch = queryExpectsExactMatch(original); let values: IPreparedQueryPiece[] | undefined = undefined; const originalSplit = original.split(MULTIPLE_QUERY_VALUES_SEPARATOR); if (originalSplit.length > 1) { for (const originalPiece of originalSplit) { + const expectExactMatchPiece = queryExpectsExactMatch(originalPiece); const { pathNormalized: pathNormalizedPiece, normalized: normalizedPiece, @@ -843,13 +868,14 @@ export function prepareQuery(original: string): IPreparedQuery { originalLowercase: originalPiece.toLowerCase(), pathNormalized: pathNormalizedPiece, normalized: normalizedPiece, - normalizedLowercase: normalizedLowercasePiece + normalizedLowercase: normalizedLowercasePiece, + expectExactMatch: expectExactMatchPiece }); } } } - return { original, originalLowercase, pathNormalized, normalized, normalizedLowercase, values, containsPathSeparator }; + return { original, originalLowercase, pathNormalized, normalized, normalizedLowercase, values, containsPathSeparator, expectExactMatch }; } function normalizeQuery(original: string): { pathNormalized: string, normalized: string, normalizedLowercase: string } { @@ -860,7 +886,8 @@ function normalizeQuery(original: string): { pathNormalized: string, normalized: pathNormalized = original.replace(/\\/g, sep); // Help macOS/Linux users to search for paths when using backslash } - const normalized = stripWildcards(pathNormalized).replace(/\s/g, ''); + // we remove quotes here because quotes are used for exact match search + const normalized = stripWildcards(pathNormalized).replace(/\s|"/g, ''); return { pathNormalized, diff --git a/src/vs/base/common/types.ts b/src/vs/base/common/types.ts index 5525a20d605..9f047e0caa9 100644 --- a/src/vs/base/common/types.ts +++ b/src/vs/base/common/types.ts @@ -290,3 +290,7 @@ export function NotImplementedProxy(name: string): { new(): T } { export function assertNever(value: never, message = 'Unreachable') { throw new Error(message); } + +export function isPromise(obj: unknown): obj is Promise { + return !!obj && typeof (obj as Promise).then === 'function' && typeof (obj as Promise).catch === 'function'; +} diff --git a/src/vs/base/test/common/fuzzyScorer.test.ts b/src/vs/base/test/common/fuzzyScorer.test.ts index 14075c91229..fa79e67ff7d 100644 --- a/src/vs/base/test/common/fuzzyScorer.test.ts +++ b/src/vs/base/test/common/fuzzyScorer.test.ts @@ -211,6 +211,13 @@ suite('Fuzzy Scorer', () => { assert.ok(!noRes.labelMatch); assert.ok(!noRes.descriptionMatch); + // No Exact Match + const noExactRes = scoreItem(resource, '"sF"', true, ResourceAccessor); + assert.ok(!noExactRes.score); + assert.ok(!noExactRes.labelMatch); + assert.ok(!noExactRes.descriptionMatch); + assert.strictEqual(noRes.score, noExactRes.score); + // Verify Scores assert.ok(identityRes.score > basenamePrefixRes.score); assert.ok(basenamePrefixRes.score > basenameRes.score); @@ -1075,9 +1082,12 @@ suite('Fuzzy Scorer', () => { assert.strictEqual(prepareQuery('model Tester.ts').original, 'model Tester.ts'); assert.strictEqual(prepareQuery('model Tester.ts').originalLowercase, 'model Tester.ts'.toLowerCase()); assert.strictEqual(prepareQuery('model Tester.ts').normalized, 'modelTester.ts'); + assert.strictEqual(prepareQuery('model Tester.ts').expectExactMatch, false); // doesn't have quotes in it assert.strictEqual(prepareQuery('Model Tester.ts').normalizedLowercase, 'modeltester.ts'); assert.strictEqual(prepareQuery('ModelTester.ts').containsPathSeparator, false); assert.strictEqual(prepareQuery('Model' + sep + 'Tester.ts').containsPathSeparator, true); + assert.strictEqual(prepareQuery('"hello"').expectExactMatch, true); + assert.strictEqual(prepareQuery('"hello"').normalized, 'hello'); // with spaces let query = prepareQuery('He*llo World'); diff --git a/src/vs/code/browser/workbench/workbench-dev.html b/src/vs/code/browser/workbench/workbench-dev.html index 21a099e5465..624cdb56edb 100644 --- a/src/vs/code/browser/workbench/workbench-dev.html +++ b/src/vs/code/browser/workbench/workbench-dev.html @@ -28,12 +28,13 @@ + - + diff --git a/src/vs/code/browser/workbench/workbench.html b/src/vs/code/browser/workbench/workbench.html index 85c010c0dd8..885c93c940c 100644 --- a/src/vs/code/browser/workbench/workbench.html +++ b/src/vs/code/browser/workbench/workbench.html @@ -27,12 +27,13 @@ + - diff --git a/src/vs/platform/browser/contextScopedHistoryWidget.ts b/src/vs/platform/browser/contextScopedHistoryWidget.ts index c43f658ae73..fc00d7dfd23 100644 --- a/src/vs/platform/browser/contextScopedHistoryWidget.ts +++ b/src/vs/platform/browser/contextScopedHistoryWidget.ts @@ -13,7 +13,8 @@ import { ContextKeyExpr, IContextKey, IContextKeyService, IContextKeyServiceTarg import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; export const HistoryNavigationWidgetContext = 'historyNavigationWidget'; -export const HistoryNavigationEnablementContext = 'historyNavigationEnabled'; +const HistoryNavigationForwardsEnablementContext = 'historyNavigationForwardsEnabled'; +const HistoryNavigationBackwardsEnablementContext = 'historyNavigationBackwardsEnabled'; function bindContextScopedWidget(contextKeyService: IContextKeyService, widget: IContextScopedWidget, contextKey: string): void { new RawContextKey(contextKey, widget).bindTo(contextKeyService); @@ -35,11 +36,22 @@ interface IContextScopedHistoryNavigationWidget extends IContextScopedWidget { historyNavigator: IHistoryNavigationWidget; } -export function createAndBindHistoryNavigationWidgetScopedContextKeyService(contextKeyService: IContextKeyService, widget: IContextScopedHistoryNavigationWidget): { scopedContextKeyService: IContextKeyService, historyNavigationEnablement: IContextKey } { +export interface IHistoryNavigationContext { + scopedContextKeyService: IContextKeyService, + historyNavigationForwardsEnablement: IContextKey, + historyNavigationBackwardsEnablement: IContextKey, +} + +export function createAndBindHistoryNavigationWidgetScopedContextKeyService(contextKeyService: IContextKeyService, widget: IContextScopedHistoryNavigationWidget): IHistoryNavigationContext { const scopedContextKeyService = createWidgetScopedContextKeyService(contextKeyService, widget); bindContextScopedWidget(scopedContextKeyService, widget, HistoryNavigationWidgetContext); - const historyNavigationEnablement = new RawContextKey(HistoryNavigationEnablementContext, true).bindTo(scopedContextKeyService); - return { scopedContextKeyService, historyNavigationEnablement }; + const historyNavigationForwardsEnablement = new RawContextKey(HistoryNavigationForwardsEnablementContext, true).bindTo(scopedContextKeyService); + const historyNavigationBackwardsEnablement = new RawContextKey(HistoryNavigationBackwardsEnablementContext, true).bindTo(scopedContextKeyService); + return { + scopedContextKeyService, + historyNavigationForwardsEnablement, + historyNavigationBackwardsEnablement, + }; } export class ContextScopedHistoryInputBox extends HistoryInputBox { @@ -77,10 +89,10 @@ export class ContextScopedReplaceInput extends ReplaceInput { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'history.showPrevious', weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(ContextKeyExpr.has(HistoryNavigationWidgetContext), ContextKeyExpr.equals(HistoryNavigationEnablementContext, true)), + when: ContextKeyExpr.and(ContextKeyExpr.has(HistoryNavigationWidgetContext), ContextKeyExpr.equals(HistoryNavigationBackwardsEnablementContext, true)), primary: KeyCode.UpArrow, secondary: [KeyMod.Alt | KeyCode.UpArrow], - handler: (accessor, arg2) => { + handler: (accessor) => { const widget = getContextScopedWidget(accessor.get(IContextKeyService), HistoryNavigationWidgetContext); if (widget) { const historyInputBox: IHistoryNavigationWidget = widget.historyNavigator; @@ -92,10 +104,10 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'history.showNext', weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(ContextKeyExpr.has(HistoryNavigationWidgetContext), ContextKeyExpr.equals(HistoryNavigationEnablementContext, true)), + when: ContextKeyExpr.and(ContextKeyExpr.has(HistoryNavigationWidgetContext), ContextKeyExpr.equals(HistoryNavigationForwardsEnablementContext, true)), primary: KeyCode.DownArrow, secondary: [KeyMod.Alt | KeyCode.DownArrow], - handler: (accessor, arg2) => { + handler: (accessor) => { const widget = getContextScopedWidget(accessor.get(IContextKeyService), HistoryNavigationWidgetContext); if (widget) { const historyInputBox: IHistoryNavigationWidget = widget.historyNavigator; diff --git a/src/vs/platform/environment/node/stdin.ts b/src/vs/platform/environment/node/stdin.ts index d5ab11b21ed..93c08e1d0b9 100644 --- a/src/vs/platform/environment/node/stdin.ts +++ b/src/vs/platform/environment/node/stdin.ts @@ -36,7 +36,7 @@ export function stdinDataListener(durationinMs: number): Promise { } export function getStdinFilePath(): string { - return paths.join(os.tmpdir(), `code-stdin-${Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 3)}.txt`); + return paths.join(os.tmpdir(), `code-stdin-${Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 3)}`); } export async function readFromStdin(targetPath: string, verbose: boolean): Promise { diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index a9338c15c97..3ae78ec6ef7 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -89,6 +89,7 @@ export const enum TerminalSettingId { LocalEchoExcludePrograms = 'terminal.integrated.localEchoExcludePrograms', LocalEchoStyle = 'terminal.integrated.localEchoStyle', EnablePersistentSessions = 'terminal.integrated.enablePersistentSessions', + CustomGlyphs = 'terminal.integrated.customGlyphs', PersistentSessionScrollback = 'terminal.integrated.persistentSessionScrollback', PersistentSessionExperimentalSerializer = 'terminal.integrated.persistentSessionExperimentalSerializer', InheritEnv = 'terminal.integrated.inheritEnv', @@ -391,13 +392,17 @@ export interface IShellLaunchConfig { } export interface ICreateContributedTerminalProfileOptions { - target?: TerminalLocation; icon?: URI | string | { light: URI, dark: URI }; color?: string; - isSplitTerminal?: boolean; + splitActiveTerminal?: boolean; } -export const enum TerminalLocation { +export enum TerminalLocation { + Panel = 0, + Editor = 1 +} + +export const enum TerminalLocationString { TerminalView = 'view', Editor = 'editor' } diff --git a/src/vs/platform/terminal/common/terminalPlatformConfiguration.ts b/src/vs/platform/terminal/common/terminalPlatformConfiguration.ts index 6f29d44d457..5531d4036a9 100644 --- a/src/vs/platform/terminal/common/terminalPlatformConfiguration.ts +++ b/src/vs/platform/terminal/common/terminalPlatformConfiguration.ts @@ -109,28 +109,28 @@ const terminalPlatformConfiguration: IConfigurationNode = { }, [TerminalSettingId.ShellLinux]: { restricted: true, - markdownDescription: localize('terminal.integrated.shell.linux', "The path of the shell that the terminal uses on Linux. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."), + markdownDescription: localize('terminal.integrated.shell.linux', "The path of the shell that the terminal uses on Linux. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_terminal-profiles)."), type: ['string', 'null'], default: null, markdownDeprecationMessage: shellDeprecationMessageLinux }, [TerminalSettingId.ShellMacOs]: { restricted: true, - markdownDescription: localize('terminal.integrated.shell.osx', "The path of the shell that the terminal uses on macOS. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."), + markdownDescription: localize('terminal.integrated.shell.osx', "The path of the shell that the terminal uses on macOS. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_terminal-profiles)."), type: ['string', 'null'], default: null, markdownDeprecationMessage: shellDeprecationMessageOsx }, [TerminalSettingId.ShellWindows]: { restricted: true, - markdownDescription: localize('terminal.integrated.shell.windows', "The path of the shell that the terminal uses on Windows. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."), + markdownDescription: localize('terminal.integrated.shell.windows', "The path of the shell that the terminal uses on Windows. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_terminal-profiles)."), type: ['string', 'null'], default: null, markdownDeprecationMessage: shellDeprecationMessageWindows }, [TerminalSettingId.ShellArgsLinux]: { restricted: true, - markdownDescription: localize('terminal.integrated.shellArgs.linux', "The command line arguments to use when on the Linux terminal. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."), + markdownDescription: localize('terminal.integrated.shellArgs.linux', "The command line arguments to use when on the Linux terminal. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_terminal-profiles)."), type: 'array', items: { type: 'string' @@ -140,7 +140,7 @@ const terminalPlatformConfiguration: IConfigurationNode = { }, [TerminalSettingId.ShellArgsMacOs]: { restricted: true, - markdownDescription: localize('terminal.integrated.shellArgs.osx', "The command line arguments to use when on the macOS terminal. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."), + markdownDescription: localize('terminal.integrated.shellArgs.osx', "The command line arguments to use when on the macOS terminal. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_terminal-profiles)."), type: 'array', items: { type: 'string' @@ -153,18 +153,18 @@ const terminalPlatformConfiguration: IConfigurationNode = { }, [TerminalSettingId.ShellArgsWindows]: { restricted: true, - markdownDescription: localize('terminal.integrated.shellArgs.windows', "The command line arguments to use when on the Windows terminal. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."), + markdownDescription: localize('terminal.integrated.shellArgs.windows', "The command line arguments to use when on the Windows terminal. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_terminal-profiles)."), 'anyOf': [ { type: 'array', items: { type: 'string', - markdownDescription: localize('terminal.integrated.shellArgs.windows', "The command line arguments to use when on the Windows terminal. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration).") + markdownDescription: localize('terminal.integrated.shellArgs.windows', "The command line arguments to use when on the Windows terminal. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_terminal-profiles).") }, }, { type: 'string', - markdownDescription: localize('terminal.integrated.shellArgs.windows.string', "The command line arguments in [command-line format](https://msdn.microsoft.com/en-au/08dfcab2-eb6e-49a4-80eb-87d4076c98c6) to use when on the Windows terminal. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration).") + markdownDescription: localize('terminal.integrated.shellArgs.windows.string', "The command line arguments in [command-line format](https://msdn.microsoft.com/en-au/08dfcab2-eb6e-49a4-80eb-87d4076c98c6) to use when on the Windows terminal. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_terminal-profiles).") } ], default: [], diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 9d9f9c57f48..7712c9887b8 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -917,6 +917,42 @@ declare module 'vscode' { readonly state: TerminalState; } + export interface TerminalOptions { + location?: TerminalLocation | TerminalEditorLocationOptions | TerminalSplitLocationOptions; + } + + export interface ExtensionTerminalOptions { + location?: TerminalLocation | TerminalEditorLocationOptions | TerminalSplitLocationOptions; + } + + export enum TerminalLocation { + Panel = 0, + Editor = 1, + } + + export interface TerminalEditorLocationOptions { + /** + * A view column in which the {@link Terminal terminal} should be shown in the editor area. + * Use {@link ViewColumn.Active active} to open in the active editor group, other values are + * adjusted to be `Min(column, columnCount + 1)`, the + * {@link ViewColumn.Active active}-column is not adjusted. Use + * {@linkcode ViewColumn.Beside} to open the editor to the side of the currently active one. + */ + viewColumn: ViewColumn; + /** + * An optional flag that when `true` will stop the {@link Terminal} from taking focus. + */ + preserveFocus?: boolean; + } + + export interface TerminalSplitLocationOptions { + /** + * The parent terminal to split this terminal beside. This works whether the parent terminal + * is in the panel or the editor area. + */ + parentTerminal: Terminal; + } + /** * An event representing a change in a {@link Terminal.state terminal's state}. */ @@ -935,7 +971,7 @@ declare module 'vscode' { /** * An {@link Event} which fires when a {@link Terminal.state terminal's state} has changed. */ - export const onDidChangeTerminalState: Event; + export const onDidChangeTerminalState: Event; } //#endregion diff --git a/src/vs/workbench/api/browser/mainThreadSearch.ts b/src/vs/workbench/api/browser/mainThreadSearch.ts index ac7014a82ba..b720d55c719 100644 --- a/src/vs/workbench/api/browser/mainThreadSearch.ts +++ b/src/vs/workbench/api/browser/mainThreadSearch.ts @@ -6,6 +6,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { dispose, IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { IFileMatch, IFileQuery, IRawFileMatch2, ISearchComplete, ISearchCompleteStats, ISearchProgressItem, ISearchResultProvider, ISearchService, ITextQuery, QueryType, SearchProviderType } from 'vs/workbench/services/search/common/search'; @@ -20,9 +21,15 @@ export class MainThreadSearch implements MainThreadSearchShape { constructor( extHostContext: IExtHostContext, @ISearchService private readonly _searchService: ISearchService, - @ITelemetryService private readonly _telemetryService: ITelemetryService + @ITelemetryService private readonly _telemetryService: ITelemetryService, + @IConfigurationService _configurationService: IConfigurationService, ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostSearch); + + const forceEHSearch = _configurationService.getValue('search.experimental.forceExtensionHostSearch'); + if (forceEHSearch) { + this._proxy.$enableExtensionHostSearch(); + } } dispose(): void { diff --git a/src/vs/workbench/api/browser/mainThreadTerminalService.ts b/src/vs/workbench/api/browser/mainThreadTerminalService.ts index 54703b9df71..7029bcd3855 100644 --- a/src/vs/workbench/api/browser/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/browser/mainThreadTerminalService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { DisposableStore, Disposable, IDisposable } from 'vs/base/common/lifecycle'; -import { ExtHostContext, ExtHostTerminalServiceShape, MainThreadTerminalServiceShape, MainContext, IExtHostContext, TerminalLaunchConfig, ITerminalDimensionsDto, TerminalIdentifier } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostContext, ExtHostTerminalServiceShape, MainThreadTerminalServiceShape, MainContext, IExtHostContext, TerminalLaunchConfig, ITerminalDimensionsDto, ExtHostTerminalIdentifier } from 'vs/workbench/api/common/extHost.protocol'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { URI } from 'vs/base/common/uri'; import { StopWatch } from 'vs/base/common/stopwatch'; @@ -20,6 +20,7 @@ import { IStartExtensionTerminalRequest, ITerminalProcessExtHostProxy, ITerminal import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { withNullAsUndefined } from 'vs/base/common/types'; import { OperatingSystem, OS } from 'vs/base/common/platform'; +import { TerminalEditorLocationOptions } from 'vscode'; @extHostNamedCustomer(MainContext.MainThreadTerminalService) export class MainThreadTerminalService implements MainThreadTerminalServiceShape { @@ -110,7 +111,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape this._proxy.$acceptDefaultProfile(...await Promise.all([defaultProfile, defaultAutomationProfile])); } - private async _getTerminalInstance(id: TerminalIdentifier): Promise { + private async _getTerminalInstance(id: ExtHostTerminalIdentifier): Promise { if (typeof id === 'string') { return this._extHostTerminals.get(id); } @@ -137,27 +138,26 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape extHostTerminalId, isFeatureTerminal: launchConfig.isFeatureTerminal, isExtensionOwnedTerminal: launchConfig.isExtensionOwnedTerminal, - useShellEnvironment: launchConfig.useShellEnvironment + useShellEnvironment: launchConfig.useShellEnvironment, }; this._extHostTerminals.set(extHostTerminalId, new Promise(async r => { - let terminal: ITerminalInstance | undefined; - if (launchConfig.isSplitTerminal) { - const activeInstance = this._terminalService.getInstanceHost(launchConfig.target).activeInstance; - if (activeInstance) { - terminal = withNullAsUndefined(await this._terminalService.createTerminal({ instanceToSplit: activeInstance, config: shellLaunchConfig })); - } - } - if (!terminal) { - terminal = await this._terminalService.createTerminal({ - config: shellLaunchConfig, - target: launchConfig.target - }); - } + const terminal = await this._terminalService.createTerminal({ + config: shellLaunchConfig, + location: await this._deserializeParentTerminal(launchConfig.location) + }); r(terminal); })); } - public async $show(id: TerminalIdentifier, preserveFocus: boolean): Promise { + private async _deserializeParentTerminal(location?: TerminalLocation | TerminalEditorLocationOptions | { parentTerminal: ExtHostTerminalIdentifier } | { splitActiveTerminal: boolean }): Promise { + if (typeof location === 'object' && 'parentTerminal' in location) { + const parentTerminal = await this._extHostTerminals.get(location.parentTerminal.toString()); + return parentTerminal ? { parentTerminal } : undefined; + } + return location; + } + + public async $show(id: ExtHostTerminalIdentifier, preserveFocus: boolean): Promise { const terminalInstance = await this._getTerminalInstance(id); if (terminalInstance) { this._terminalService.setActiveInstance(terminalInstance); @@ -169,7 +169,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape } } - public async $hide(id: TerminalIdentifier): Promise { + public async $hide(id: ExtHostTerminalIdentifier): Promise { const instanceToHide = await this._getTerminalInstance(id); const activeInstance = this._terminalService.activeInstance; if (activeInstance && activeInstance.instanceId === instanceToHide?.instanceId && activeInstance.target !== TerminalLocation.Editor) { @@ -177,11 +177,11 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape } } - public async $dispose(id: TerminalIdentifier): Promise { + public async $dispose(id: ExtHostTerminalIdentifier): Promise { (await this._getTerminalInstance(id))?.dispose(); } - public async $sendText(id: TerminalIdentifier, text: string, addNewLine: boolean): Promise { + public async $sendText(id: ExtHostTerminalIdentifier, text: string, addNewLine: boolean): Promise { const instance = await this._getTerminalInstance(id); await instance?.sendText(text, addNewLine); } diff --git a/src/vs/workbench/api/browser/mainThreadTesting.ts b/src/vs/workbench/api/browser/mainThreadTesting.ts index 9dbf777b199..8bb2b187745 100644 --- a/src/vs/workbench/api/browser/mainThreadTesting.ts +++ b/src/vs/workbench/api/browser/mainThreadTesting.ts @@ -200,7 +200,6 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh id: controllerId, label, configureRunProfile: id => this.proxy.$configureRunProfile(controllerId, id), - getTags: () => this.proxy.$getTestTags(controllerId), runTests: (req, token) => this.proxy.$runControllerTests(req, token), expandTest: (testId, levels) => this.proxy.$expandTest(testId, isFinite(levels) ? levels : -1), }; diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 9fd85f03e28..8a401cd930c 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -646,6 +646,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I }, createTerminal(nameOrOptions?: vscode.TerminalOptions | vscode.ExtensionTerminalOptions | string, shellPath?: string, shellArgs?: string[] | string): vscode.Terminal { if (typeof nameOrOptions === 'object') { + if ('location' in nameOrOptions) { + checkProposedApiEnabled(extension); + } if ('pty' in nameOrOptions) { return extHostTerminalService.createExtensionTerminal(nameOrOptions); } @@ -1234,6 +1237,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I TaskRevealKind: extHostTypes.TaskRevealKind, TaskScope: extHostTypes.TaskScope, TerminalLink: extHostTypes.TerminalLink, + TerminalLocation: extHostTypes.TerminalLocation, TerminalProfile: extHostTypes.TerminalProfile, TextDocumentSaveReason: extHostTypes.TextDocumentSaveReason, TextEdit: extHostTypes.TextEdit, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index f9cce1c5246..e5239069390 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -60,7 +60,7 @@ import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { InputValidationType } from 'vs/workbench/contrib/scm/common/scm'; import { ITextQueryBuilderOptions } from 'vs/workbench/contrib/search/common/queryBuilder'; import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; -import { CoverageDetails, ExtensionRunTestsRequest, IFileCoverage, ISerializedTestResults, ITestItem, ITestMessage, ITestRunProfile, ITestRunTask, ITestTagDisplayInfo, ResolvedTestRunRequest, RunTestForControllerRequest, TestResultState, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { CoverageDetails, ExtensionRunTestsRequest, IFileCoverage, ISerializedTestResults, ITestItem, ITestMessage, ITestRunProfile, ITestRunTask, ResolvedTestRunRequest, RunTestForControllerRequest, TestResultState, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; import { InternalTimelineOptions, Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor } from 'vs/workbench/contrib/timeline/common/timeline'; import { TypeHierarchyItem } from 'vs/workbench/contrib/typeHierarchy/common/typeHierarchy'; import { EditorGroupColumn } from 'vs/workbench/services/editor/common/editorGroupColumn'; @@ -467,7 +467,7 @@ export interface MainThreadProgressShape extends IDisposable { * All other terminals (that are not created on the extension host side) always * use the numeric id. */ -export type TerminalIdentifier = number | string; +export type ExtHostTerminalIdentifier = number | string; export interface TerminalLaunchConfig { name?: string; @@ -485,16 +485,15 @@ export interface TerminalLaunchConfig { isFeatureTerminal?: boolean; isExtensionOwnedTerminal?: boolean; useShellEnvironment?: boolean; - isSplitTerminal?: boolean; - target?: TerminalLocation; + location?: TerminalLocation | { viewColumn: number, preserveFocus?: boolean } | { parentTerminal: ExtHostTerminalIdentifier } | { splitActiveTerminal: boolean }; } export interface MainThreadTerminalServiceShape extends IDisposable { $createTerminal(extHostTerminalId: string, config: TerminalLaunchConfig): Promise; - $dispose(id: TerminalIdentifier): void; - $hide(id: TerminalIdentifier): void; - $sendText(id: TerminalIdentifier, text: string, addNewLine: boolean): void; - $show(id: TerminalIdentifier, preserveFocus: boolean): void; + $dispose(id: ExtHostTerminalIdentifier): void; + $hide(id: ExtHostTerminalIdentifier): void; + $sendText(id: ExtHostTerminalIdentifier, text: string, addNewLine: boolean): void; + $show(id: ExtHostTerminalIdentifier, preserveFocus: boolean): void; $startSendingDataEvents(): void; $stopSendingDataEvents(): void; $startLinkProvider(): void; @@ -1314,6 +1313,7 @@ export interface ExtHostSecretStateShape { } export interface ExtHostSearchShape { + $enableExtensionHostSearch(): void; $provideFileSearchResults(handle: number, session: number, query: search.IRawQuery, token: CancellationToken): Promise; $provideTextSearchResults(handle: number, session: number, query: search.IRawTextQuery, token: CancellationToken): Promise; $clearCache(cacheKey: string): Promise; @@ -2122,8 +2122,6 @@ export interface ExtHostTestingShape { $resolveFileCoverage(runId: string, taskId: string, fileIndex: number, token: CancellationToken): Promise; /** Configures a test run config. */ $configureRunProfile(controllerId: string, configId: number): void; - /** Gets all in-use tags from the controller. */ - $getTestTags(controllerId: string): Promise; } export interface MainThreadTestingShape { diff --git a/src/vs/workbench/api/common/extHostSearch.ts b/src/vs/workbench/api/common/extHostSearch.ts index 7ed796455c5..ff6e93020d1 100644 --- a/src/vs/workbench/api/common/extHostSearch.ts +++ b/src/vs/workbench/api/common/extHostSearch.ts @@ -105,11 +105,13 @@ export class ExtHostSearch implements ExtHostSearchShape { return engine.search(progress => this._proxy.$handleTextMatch(handle, session, progress), token); } + $enableExtensionHostSearch(): void { } + protected createTextSearchManager(query: ITextQuery, provider: vscode.TextSearchProvider): TextSearchManager { return new TextSearchManager(query, provider, { readdir: resource => Promise.resolve([]), // TODO@rob implement toCanonicalName: encoding => encoding - }); + }, 'textSearchProvider'); } } diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index 01134e8ff14..f78b6ec3ac3 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -5,7 +5,7 @@ import type * as vscode from 'vscode'; import { Event, Emitter } from 'vs/base/common/event'; -import { ExtHostTerminalServiceShape, MainContext, MainThreadTerminalServiceShape, ITerminalDimensionsDto, ITerminalLinkDto, TerminalIdentifier } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostTerminalServiceShape, MainContext, MainThreadTerminalServiceShape, ITerminalDimensionsDto, ITerminalLinkDto, ExtHostTerminalIdentifier } from 'vs/workbench/api/common/extHost.protocol'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { URI } from 'vs/base/common/uri'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; @@ -34,7 +34,7 @@ export interface IExtHostTerminalService extends ExtHostTerminalServiceShape, ID onDidOpenTerminal: Event; onDidChangeActiveTerminal: Event; onDidChangeTerminalDimensions: Event; - onDidChangeTerminalState: Event; + onDidChangeTerminalState: Event; onDidWriteTerminalData: Event; createTerminal(name?: string, shellPath?: string, shellArgs?: string[] | string): vscode.Terminal; @@ -51,8 +51,8 @@ export interface IExtHostTerminalService extends ExtHostTerminalServiceShape, ID export interface ITerminalInternalOptions { isFeatureTerminal?: boolean; useShellEnvironment?: boolean; - isSplitTerminal?: boolean; - target?: TerminalLocation; + resolvedExtHostIdentifier?: ExtHostTerminalIdentifier; + splitActiveTerminal?: boolean; } export const IExtHostTerminalService = createDecorator('IExtHostTerminalService'); @@ -72,7 +72,7 @@ export class ExtHostTerminal { constructor( private _proxy: MainThreadTerminalServiceShape, - public _id: TerminalIdentifier, + public _id: ExtHostTerminalIdentifier, private readonly _creationOptions: vscode.TerminalOptions | vscode.ExtensionTerminalOptions, private _name?: string, ) { @@ -147,12 +147,12 @@ export class ExtHostTerminal { isFeatureTerminal: withNullAsUndefined(internalOptions?.isFeatureTerminal), isExtensionOwnedTerminal: true, useShellEnvironment: withNullAsUndefined(internalOptions?.useShellEnvironment), - isSplitTerminal: internalOptions?.isSplitTerminal, - target: internalOptions?.target + location: this._serializeParentTerminal(options.location, internalOptions?.resolvedExtHostIdentifier, internalOptions?.splitActiveTerminal) }); } - public async createExtensionTerminal(isSplitTerminal?: boolean, target?: TerminalLocation, iconPath?: TerminalIcon, color?: ThemeColor): Promise { + + public async createExtensionTerminal(location?: TerminalLocation | vscode.TerminalEditorLocationOptions | vscode.TerminalSplitLocationOptions, parentTerminal?: ExtHostTerminalIdentifier, iconPath?: TerminalIcon, color?: ThemeColor): Promise { if (typeof this._id !== 'string') { throw new Error('Terminal has already been created'); } @@ -161,8 +161,7 @@ export class ExtHostTerminal { isExtensionCustomPtyTerminal: true, icon: iconPath, color: ThemeColor.isThemeColor(color) ? color.id : undefined, - isSplitTerminal, - target + location: this._serializeParentTerminal(location, parentTerminal) }); // At this point, the id has been set via `$acceptTerminalOpened` if (typeof this._id === 'string') { @@ -171,6 +170,15 @@ export class ExtHostTerminal { return this._id; } + private _serializeParentTerminal(location?: TerminalLocation | vscode.TerminalEditorLocationOptions | vscode.TerminalSplitLocationOptions, parentTerminal?: ExtHostTerminalIdentifier, splitActiveTerminal?: boolean): TerminalLocation | vscode.TerminalEditorLocationOptions | { parentTerminal: ExtHostTerminalIdentifier } | { splitActiveTerminal: boolean } | undefined { + if (typeof location === 'object' && 'parentTerminal' in location) { + return parentTerminal ? { parentTerminal } : undefined; + } else if (splitActiveTerminal) { + return { splitActiveTerminal: true }; + } + return location; + } + private _checkDisposed() { if (this._disposed) { throw new Error('Terminal has already been disposed'); @@ -351,7 +359,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I readonly onDidChangeActiveTerminal = this._onDidChangeActiveTerminal.event; protected readonly _onDidChangeTerminalDimensions = new Emitter(); readonly onDidChangeTerminalDimensions = this._onDidChangeTerminalDimensions.event; - protected readonly _onDidChangeTerminalState = new Emitter(); + protected readonly _onDidChangeTerminalState = new Emitter(); readonly onDidChangeTerminalState = this._onDidChangeTerminalState.event; protected readonly _onDidWriteTerminalData: Emitter; get onDidWriteTerminalData(): Event { return this._onDidWriteTerminalData.event; } @@ -393,7 +401,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I public createExtensionTerminal(options: vscode.ExtensionTerminalOptions, internalOptions?: ITerminalInternalOptions): vscode.Terminal { const terminal = new ExtHostTerminal(this._proxy, generateUuid(), options, options.name); const p = new ExtHostPseudoterminal(options.pty); - terminal.createExtensionTerminal(internalOptions?.isSplitTerminal, internalOptions?.target, asTerminalIcon(options.iconPath), asTerminalColor(options.color)).then(id => { + terminal.createExtensionTerminal(this._resolveLocation(options.location), internalOptions?.resolvedExtHostIdentifier, asTerminalIcon(options.iconPath), asTerminalColor(options.color)).then(id => { const disposable = this._setupExtHostProcessListeners(id, p); this._terminalProcessDisposables[id] = disposable; }); @@ -401,6 +409,13 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I return terminal.value; } + private _resolveLocation(location?: TerminalLocation | vscode.TerminalEditorLocationOptions | vscode.TerminalSplitLocationOptions): undefined | TerminalLocation | vscode.TerminalEditorLocationOptions { + if (typeof location === 'object' && 'parentTerminal' in location) { + return undefined; + } + return location; + } + public attachPtyToTerminal(id: number, pty: vscode.Pseudoterminal): void { const terminal = this._getTerminalById(id); if (!terminal) { @@ -571,10 +586,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I public $acceptTerminalInteraction(id: number): void { const terminal = this._getTerminalById(id); if (terminal?.setInteractedWith()) { - this._onDidChangeTerminalState.fire({ - terminal: terminal.value, - state: terminal.value.state - }); + this._onDidChangeTerminalState.fire(terminal.value); } } @@ -639,6 +651,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I if (!profile || !('options' in profile)) { throw new Error(`No terminal profile options provided for id "${id}"`); } + if ('pty' in profile.options) { this.createExtensionTerminal(profile.options, options); return; @@ -742,7 +755,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I return index !== null ? array[index] : null; } - private _getTerminalObjectIndexById(array: T[], id: TerminalIdentifier): number | null { + private _getTerminalObjectIndexById(array: T[], id: ExtHostTerminalIdentifier): number | null { let index: number | null = null; array.some((item, i) => { const thisId = item._id; diff --git a/src/vs/workbench/api/common/extHostTesting.ts b/src/vs/workbench/api/common/extHostTesting.ts index 3965361c9e7..a1c85f54bf3 100644 --- a/src/vs/workbench/api/common/extHostTesting.ts +++ b/src/vs/workbench/api/common/extHostTesting.ts @@ -21,7 +21,7 @@ import { InvalidTestItemError, TestItemImpl, TestItemRootImpl } from 'vs/workben import * as Convert from 'vs/workbench/api/common/extHostTypeConverters'; import { TestRunProfileKind, TestRunRequest } from 'vs/workbench/api/common/extHostTypes'; import { SingleUseTestCollection } from 'vs/workbench/contrib/testing/common/ownedTestCollection'; -import { AbstractIncrementalTestCollection, CoverageDetails, IFileCoverage, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, ISerializedTestResults, ITestItem, ITestTagDisplayInfo, RunTestForControllerRequest, TestResultState, TestRunProfileBitset, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { AbstractIncrementalTestCollection, CoverageDetails, IFileCoverage, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, ISerializedTestResults, ITestItem, RunTestForControllerRequest, TestResultState, TestRunProfileBitset, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; import { TestId, TestIdPathParts, TestPosition } from 'vs/workbench/contrib/testing/common/testId'; import type * as vscode from 'vscode'; @@ -176,29 +176,6 @@ export class ExtHostTesting implements ExtHostTestingShape { this.controllers.get(controllerId)?.profiles.get(profileId)?.configureHandler?.(); } - /** @inheritdoc */ - $getTestTags(controllerId: string) { - const record = this.controllers.get(controllerId); - if (!record) { - return Promise.resolve([]); - } - - const tags = new Map(); - for (const profile of record.profiles.values()) { - if (profile.tag) { - const display = Convert.TestTag.display(controllerId, profile.tag); - tags.set(display.id, display); - } - } - - for (const tag of record.collection.tags()) { - const display = Convert.TestTag.display(controllerId, tag); - tags.set(display.id, display); - } - - return Promise.resolve([...tags.values()]); - } - /** * Updates test results shown to extensions. * @override diff --git a/src/vs/workbench/api/common/extHostTestingPrivateApi.ts b/src/vs/workbench/api/common/extHostTestingPrivateApi.ts index d44b48f4cf1..270de0bd118 100644 --- a/src/vs/workbench/api/common/extHostTestingPrivateApi.ts +++ b/src/vs/workbench/api/common/extHostTestingPrivateApi.ts @@ -32,6 +32,7 @@ export interface ITestItemSetProp { op: ExtHostTestItemEventOp.SetProp; key: keyof vscode.TestItem; value: any; + previous: any; } export interface ITestItemBulkReplace { op: ExtHostTestItemEventOp.Bulk; @@ -81,8 +82,14 @@ const testItemPropAccessor = ( }, set(newValue: vscode.TestItem[K]) { if (!equals(value, newValue)) { + const oldValue = value; value = newValue; - api.listener?.({ op: ExtHostTestItemEventOp.SetProp, key, value: newValue }); + api.listener?.({ + op: ExtHostTestItemEventOp.SetProp, + key, + value: newValue, + previous: oldValue, + }); } }, }; diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 1c39f5887b9..f10a80a7bc7 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -32,7 +32,7 @@ import { SaveReason } from 'vs/workbench/common/editor'; import * as notebooks from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; import * as search from 'vs/workbench/contrib/search/common/search'; -import { CoverageDetails, DetailType, ICoveredCount, IFileCoverage, ISerializedTestResults, ITestErrorMessage, ITestItem, ITestItemContext, ITestTag, ITestTagDisplayInfo, SerializedTestResultItem, TestMessageType } from 'vs/workbench/contrib/testing/common/testCollection'; +import { CoverageDetails, DetailType, ICoveredCount, IFileCoverage, ISerializedTestResults, ITestErrorMessage, ITestItem, ITestItemContext, ITestTag, SerializedTestResultItem, TestMessageType } from 'vs/workbench/contrib/testing/common/testCollection'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { EditorGroupColumn } from 'vs/workbench/services/editor/common/editorGroupColumn'; import { ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; @@ -1666,14 +1666,6 @@ export namespace TestTag { export const namespace = (ctrlId: string, tagId: string) => ctrlId + Constants.Delimiter + tagId; - export function display(controllerId: string, tag: vscode.TestTag): ITestTagDisplayInfo { - return { - displayId: tag.id, - id: namespace(controllerId, tag.id), - label: tag.label, - }; - } - export const denamespace = (namespaced: string) => { const index = namespaced.indexOf(Constants.Delimiter); return { ctrlId: namespaced.slice(0, index), tagId: namespaced.slice(index + 1) }; diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index f1f9795d7f2..6aaa5cb18eb 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -1735,6 +1735,11 @@ export class TerminalLink implements vscode.TerminalLink { } } +export enum TerminalLocation { + Panel = 0, + Editor = 1, +} + export class TerminalProfile implements vscode.TerminalProfile { constructor( public options: vscode.TerminalOptions | vscode.ExtensionTerminalOptions @@ -3335,7 +3340,11 @@ export class TestTag implements vscode.TestTag { constructor( public readonly id: string, public readonly label?: string, - ) { } + ) { + if (/\s/.test(id)) { + throw new Error(`Test tag ID "${id}" may not include whitespace`); + } + } } //#endregion diff --git a/src/vs/workbench/api/node/extHostSearch.ts b/src/vs/workbench/api/node/extHostSearch.ts index be31f4ad1e0..1c24658fd75 100644 --- a/src/vs/workbench/api/node/extHostSearch.ts +++ b/src/vs/workbench/api/node/extHostSearch.ts @@ -4,21 +4,21 @@ *--------------------------------------------------------------------------------------------*/ import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import * as pfs from 'vs/base/node/pfs'; import { ILogService } from 'vs/platform/log/common/log'; -import { IFileQuery, IRawFileQuery, ISearchCompleteStats, isSerializedFileMatch, ISerializedSearchProgressItem, ITextQuery } from 'vs/workbench/services/search/common/search'; +import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; +import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; +import { ExtHostSearch, reviveQuery } from 'vs/workbench/api/common/extHostSearch'; +import { IURITransformerService } from 'vs/workbench/api/common/extHostUriTransformerService'; +import { IFileQuery, IRawFileQuery, ISearchCompleteStats, ISerializedSearchProgressItem, isSerializedFileMatch, ITextQuery } from 'vs/workbench/services/search/common/search'; +import { TextSearchManager } from 'vs/workbench/services/search/common/textSearchManager'; import { SearchService } from 'vs/workbench/services/search/node/rawSearchService'; import { RipgrepSearchProvider } from 'vs/workbench/services/search/node/ripgrepSearchProvider'; import { OutputChannel } from 'vs/workbench/services/search/node/ripgrepSearchUtils'; -import type * as vscode from 'vscode'; -import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; -import { IURITransformerService } from 'vs/workbench/api/common/extHostUriTransformerService'; -import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; -import { ExtHostSearch, reviveQuery } from 'vs/workbench/api/common/extHostSearch'; -import { Schemas } from 'vs/base/common/network'; import { NativeTextSearchManager } from 'vs/workbench/services/search/node/textSearchManager'; -import { TextSearchManager } from 'vs/workbench/services/search/common/textSearchManager'; +import type * as vscode from 'vscode'; export class NativeExtHostSearch extends ExtHostSearch { @@ -42,10 +42,14 @@ export class NativeExtHostSearch extends ExtHostSearch { } } + override $enableExtensionHostSearch(): void { + this._registerEHSearchProviders(); + } + private _registerEHSearchProviders(): void { const outputChannel = new OutputChannel('RipgrepSearchEH', this._logService); this.registerTextSearchProvider(Schemas.file, new RipgrepSearchProvider(outputChannel)); - this.registerInternalFileSearchProvider(Schemas.file, new SearchService()); + this.registerInternalFileSearchProvider(Schemas.file, new SearchService('fileSearchProvider')); } private registerInternalFileSearchProvider(scheme: string, provider: SearchService): IDisposable { @@ -100,6 +104,6 @@ export class NativeExtHostSearch extends ExtHostSearch { } protected override createTextSearchManager(query: ITextQuery, provider: vscode.TextSearchProvider): TextSearchManager { - return new NativeTextSearchManager(query, provider); + return new NativeTextSearchManager(query, provider, undefined, 'textSearchProvider'); } } diff --git a/src/vs/workbench/api/node/extHostTerminalService.ts b/src/vs/workbench/api/node/extHostTerminalService.ts index 652223e1a45..dadc49d7e45 100644 --- a/src/vs/workbench/api/node/extHostTerminalService.ts +++ b/src/vs/workbench/api/node/extHostTerminalService.ts @@ -23,7 +23,21 @@ export class ExtHostTerminalService extends BaseExtHostTerminalService { public createTerminalFromOptions(options: vscode.TerminalOptions, internalOptions?: ITerminalInternalOptions): vscode.Terminal { const terminal = new ExtHostTerminal(this._proxy, generateUuid(), options, options.name); this._terminals.push(terminal); - terminal.create(options, internalOptions); + terminal.create(options, this._serializeParentTerminal(options, internalOptions)); return terminal.value; } + + private _serializeParentTerminal(options: vscode.TerminalOptions, internalOptions?: ITerminalInternalOptions): ITerminalInternalOptions { + internalOptions = internalOptions ? internalOptions : {}; + if (options.location && typeof options.location === 'object' && 'parentTerminal' in options.location) { + const parentTerminal = options.location.parentTerminal; + if (parentTerminal) { + const parentExtHostTerminal = this._terminals.find(t => t.value === parentTerminal); + if (parentExtHostTerminal) { + internalOptions.resolvedExtHostIdentifier = parentExtHostTerminal._id; + } + } + } + return internalOptions; + } } diff --git a/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts b/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts index 2122c889173..86dd96b2142 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts @@ -24,7 +24,7 @@ import { IModelService } from 'vs/editor/common/services/modelService'; import { ContextMenuController } from 'vs/editor/contrib/contextmenu/contextmenu'; import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2'; import { SuggestController } from 'vs/editor/contrib/suggest/suggestController'; -import { IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ColorIdentifier, editorSelectionBackground, inputBackground, inputBorder, inputForeground, inputPlaceholderForeground, selectionBackground } from 'vs/platform/theme/common/colorRegistry'; import { IStyleOverrides, attachStyler } from 'vs/platform/theme/common/styler'; @@ -35,14 +35,18 @@ import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEdito import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; import { IThemable } from 'vs/base/common/styler'; import { DEFAULT_FONT_FAMILY } from 'vs/workbench/browser/style'; +import { HistoryNavigator } from 'vs/base/common/history'; +import { createAndBindHistoryNavigationWidgetScopedContextKeyService, IHistoryNavigationContext } from 'vs/platform/browser/contextScopedHistoryWidget'; +import { IHistoryNavigationWidget } from 'vs/base/browser/history'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -interface SuggestResultsProvider { +export interface SuggestResultsProvider { /** * Provider function for suggestion results. * * @param query the full text of the input. */ - provideResults: (query: string) => string[]; + provideResults: (query: string) => (modes.CompletionItemLabel | string)[]; /** * Trigger characters for this input. Suggestions will appear when one of these is typed, @@ -106,9 +110,9 @@ export class SuggestEnabledInput extends Widget implements IThemable { private readonly _onInputDidChange = new Emitter(); readonly onInputDidChange: Event = this._onInputDidChange.event; - private readonly inputWidget: CodeEditorWidget; + protected readonly inputWidget: CodeEditorWidget; private readonly inputModel: ITextModel; - private stylingContainer: HTMLDivElement; + protected stylingContainer: HTMLDivElement; private placeholderText: HTMLDivElement; constructor( @@ -118,8 +122,9 @@ export class SuggestEnabledInput extends Widget implements IThemable { ariaLabel: string, resourceHandle: string, options: SuggestEnabledInputOptions, - @IInstantiationService instantiationService: IInstantiationService, + @IInstantiationService defaultInstantiationService: IInstantiationService, @IModelService modelService: IModelService, + @IContextKeyService contextKeyService: IContextKeyService, ) { super(); @@ -130,6 +135,12 @@ export class SuggestEnabledInput extends Widget implements IThemable { getSimpleEditorOptions(), getSuggestEnabledInputOptions(ariaLabel)); + const scopedContextKeyService = this.getScopedContextKeyService(contextKeyService, parent); + + const instantiationService = scopedContextKeyService + ? defaultInstantiationService.createChild(new ServiceCollection([IContextKeyService, scopedContextKeyService])) + : defaultInstantiationService; + this.inputWidget = instantiationService.createInstance(CodeEditorWidget, this.stylingContainer, editorOptions, { @@ -199,13 +210,23 @@ export class SuggestEnabledInput extends Widget implements IThemable { } return { - suggestions: suggestionProvider.provideResults(query).map(result => { - return { - label: result, - insertText: result, + suggestions: suggestionProvider.provideResults(query).map((result): modes.CompletionItem => { + let label: string; + let rest: Partial | undefined; + if (typeof result === 'string') { + label = result; + } else { + label = result.label; + rest = result; + } + + return { + label, + insertText: label, range: Range.fromPositions(position.delta(0, -alreadyTypedCount), position), - sortText: validatedSuggestProvider.sortKey(result), - kind: modes.CompletionItemKind.Keyword + sortText: validatedSuggestProvider.sortKey(label), + kind: modes.CompletionItemKind.Keyword, + ...rest }; }) }; @@ -213,6 +234,10 @@ export class SuggestEnabledInput extends Widget implements IThemable { })); } + protected getScopedContextKeyService(_contextKeyService: IContextKeyService, _parent: HTMLElement): IContextKeyService | undefined { + return undefined; + } + public updateAriaLabel(label: string): void { this.inputWidget.updateOptions({ ariaLabel: label }); } @@ -231,7 +256,6 @@ export class SuggestEnabledInput extends Widget implements IThemable { return this.inputWidget.getValue(); } - public style(colors: ISuggestEnabledInputStyles): void { this.stylingContainer.style.backgroundColor = colors.inputBackground ? colors.inputBackground.toString() : ''; this.stylingContainer.style.color = colors.inputForeground ? colors.inputForeground.toString() : ''; @@ -271,6 +295,127 @@ export class SuggestEnabledInput extends Widget implements IThemable { } } +export interface ISuggestEnabledHistoryOptions { + id: string, + ariaLabel: string, + parent: HTMLElement, + suggestionProvider: SuggestResultsProvider, + resourceHandle: string, + suggestOptions: SuggestEnabledInputOptions, + history: string[], +} + +export class SuggestEnabledInputWithHistory extends SuggestEnabledInput implements IHistoryNavigationWidget { + protected readonly history: HistoryNavigator; + + constructor( + { id, parent, ariaLabel, suggestionProvider, resourceHandle, suggestOptions, history }: ISuggestEnabledHistoryOptions, + @IInstantiationService instantiationService: IInstantiationService, + @IModelService modelService: IModelService, + @IContextKeyService contextKeyService: IContextKeyService, + ) { + super(id, parent, suggestionProvider, ariaLabel, resourceHandle, suggestOptions, instantiationService, modelService, contextKeyService); + this.history = new HistoryNavigator(history, 100); + } + + public addToHistory(): void { + const value = this.getValue(); + if (value && value !== this.getCurrentValue()) { + this.history.add(value); + } + } + + public getHistory(): string[] { + return this.history.getHistory(); + } + + public showNextValue(): void { + if (!this.history.has(this.getValue())) { + this.addToHistory(); + } + + let next = this.getNextValue(); + if (next) { + next = next === this.getValue() ? this.getNextValue() : next; + } + + if (next) { + this.setValue(next); + } + } + + public showPreviousValue(): void { + if (!this.history.has(this.getValue())) { + this.addToHistory(); + } + + let previous = this.getPreviousValue(); + if (previous) { + previous = previous === this.getValue() ? this.getPreviousValue() : previous; + } + + if (previous) { + this.setValue(previous); + this.inputWidget.setPosition({ lineNumber: 0, column: 0 }); + } + } + + public clearHistory(): void { + this.history.clear(); + } + + private getCurrentValue(): string | null { + let currentValue = this.history.current(); + if (!currentValue) { + currentValue = this.history.last(); + this.history.next(); + } + return currentValue; + } + + private getPreviousValue(): string | null { + return this.history.previous() || this.history.first(); + } + + private getNextValue(): string | null { + return this.history.next() || this.history.last(); + } +} + +export class ContextScopedSuggestEnabledInputWithHistory extends SuggestEnabledInputWithHistory { + private historyContext!: IHistoryNavigationContext; + + constructor( + options: ISuggestEnabledHistoryOptions, + @IInstantiationService instantiationService: IInstantiationService, + @IModelService modelService: IModelService, + @IContextKeyService contextKeyService: IContextKeyService, + ) { + super(options, instantiationService, modelService, contextKeyService); + + const { historyNavigationBackwardsEnablement, historyNavigationForwardsEnablement } = this.historyContext; + this._register(this.inputWidget.onDidChangeCursorPosition(({ position }) => { + const viewModel = this.inputWidget._getViewModel()!; + const lastLineNumber = viewModel.getLineCount(); + const lastLineCol = viewModel.getLineContent(lastLineNumber).length + 1; + const viewPosition = viewModel.coordinatesConverter.convertModelPositionToViewPosition(position); + historyNavigationBackwardsEnablement.set(viewPosition.lineNumber === 1 && viewPosition.column === 1); + historyNavigationForwardsEnablement.set(viewPosition.lineNumber === lastLineNumber && viewPosition.column === lastLineCol); + })); + } + + protected override getScopedContextKeyService(contextKeyService: IContextKeyService, parent: HTMLElement) { + const scoped = this.historyContext = createAndBindHistoryNavigationWidgetScopedContextKeyService( + contextKeyService, + { target: parent, historyNavigator: this }, + ); + + this._register(scoped.scopedContextKeyService); + + return scoped.scopedContextKeyService; + } +} + // Override styles in selections.ts registerThemingParticipant((theme, collector) => { let selectionColor = theme.getColor(selectionBackground); diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index a09e3d739c1..d9572ef3095 100644 --- a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts @@ -55,6 +55,7 @@ import { DisassemblyView, DisassemblyViewContribution } from 'vs/workbench/contr import { EditorPaneDescriptor, IEditorPaneRegistry } from 'vs/workbench/browser/editor'; import { DisassemblyViewInput } from 'vs/workbench/contrib/debug/common/disassemblyViewInput'; import { Codicon } from 'vs/base/common/codicons'; +import { DebugLifecycle } from 'vs/workbench/contrib/debug/common/debugLifecycle'; const debugCategory = nls.localize('debugCategory', "Debug"); registerColors(); @@ -70,6 +71,7 @@ Registry.as(WorkbenchExtensions.Workbench).regi Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(DebugContentProvider, LifecyclePhase.Eventually); Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(StatusBarColorProvider, LifecyclePhase.Eventually); Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(DisassemblyViewContribution, LifecyclePhase.Eventually); +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(DebugLifecycle, LifecyclePhase.Eventually); // Register Quick Access Registry.as(QuickAccessExtensions.Quickaccess).registerQuickAccessProvider({ @@ -524,6 +526,16 @@ configurationRegistry.registerConfiguration({ ], default: 'allEditorsInActiveGroup', scope: ConfigurationScope.LANGUAGE_OVERRIDABLE + }, + 'debug.confirmOnExit': { + description: nls.localize('debug.confirmOnExit', "Controls whether to confirm when the window closes if there are active debug sessions."), + type: 'string', + enum: ['never', 'always'], + enumDescriptions: [ + nls.localize('debug.confirmOnExit.never', "Never confirm."), + nls.localize('debug.confirmOnExit.always', "Always confirm if there are debug sessions."), + ], + default: 'never' } } }); diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index b0455996fa4..8d377405c7e 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -95,7 +95,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { private dimension!: dom.Dimension; private replInputLineCount = 1; private model: ITextModel | undefined; - private historyNavigationEnablement!: IContextKey; + private setHistoryNavigationEnablement!: (enabled: boolean) => void; private scopedInstantiationService!: IInstantiationService; private replElementsChangeListener: IDisposable | undefined; private styleElement: HTMLStyleElement | undefined; @@ -153,7 +153,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { triggerCharacters: session.capabilities.completionTriggerCharacters || ['.'], provideCompletionItems: async (_: ITextModel, position: Position, _context: CompletionContext, token: CancellationToken): Promise => { // Disable history navigation because up and down are used to navigate through the suggest widget - this.historyNavigationEnablement.set(false); + this.setHistoryNavigationEnablement(false); const model = this.replInput.getModel(); if (model) { @@ -373,7 +373,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { aria.status(historyInput); // always leave cursor at the end. this.replInput.setPosition({ lineNumber: 1, column: historyInput.length + 1 }); - this.historyNavigationEnablement.set(true); + this.setHistoryNavigationEnablement(true); } } @@ -609,8 +609,11 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { this.replInputContainer = dom.append(container, $('.repl-input-wrapper')); dom.append(this.replInputContainer, $('.repl-input-chevron' + ThemeIcon.asCSSSelector(debugConsoleEvaluationPrompt))); - const { scopedContextKeyService, historyNavigationEnablement } = createAndBindHistoryNavigationWidgetScopedContextKeyService(this.contextKeyService, { target: container, historyNavigator: this }); - this.historyNavigationEnablement = historyNavigationEnablement; + const { scopedContextKeyService, historyNavigationBackwardsEnablement, historyNavigationForwardsEnablement } = createAndBindHistoryNavigationWidgetScopedContextKeyService(this.contextKeyService, { target: container, historyNavigator: this }); + this.setHistoryNavigationEnablement = enabled => { + historyNavigationBackwardsEnablement.set(enabled); + historyNavigationForwardsEnablement.set(enabled); + }; this._register(scopedContextKeyService); CONTEXT_IN_DEBUG_REPL.bindTo(scopedContextKeyService).set(true); @@ -626,7 +629,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { this._register(this.replInput.onDidChangeModelContent(() => { const model = this.replInput.getModel(); - this.historyNavigationEnablement.set(!!model && model.getValue() === ''); + this.setHistoryNavigationEnablement(!!model && model.getValue() === ''); const lineCount = model ? Math.min(10, model.getLineCount()) : 1; if (lineCount !== this.replInputLineCount) { this.replInputLineCount = lineCount; diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index c585d8131aa..ea4e5c14fc1 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -547,6 +547,7 @@ export interface IDebugConfiguration { onTaskErrors: 'debugAnyway' | 'showErrors' | 'prompt' | 'abort'; showBreakpointsInOverviewRuler: boolean; showInlineBreakpointCandidates: boolean; + confirmOnExit: 'always' | 'never'; } export interface IGlobalConfig { diff --git a/src/vs/workbench/contrib/debug/common/debugLifecycle.ts b/src/vs/workbench/contrib/debug/common/debugLifecycle.ts new file mode 100644 index 00000000000..5cce9304f75 --- /dev/null +++ b/src/vs/workbench/contrib/debug/common/debugLifecycle.ts @@ -0,0 +1,50 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { IDebugConfiguration, IDebugService } from 'vs/workbench/contrib/debug/common/debug'; +import { ILifecycleService, ShutdownReason } from 'vs/workbench/services/lifecycle/common/lifecycle'; + +export class DebugLifecycle implements IWorkbenchContribution { + constructor( + @ILifecycleService lifecycleService: ILifecycleService, + @IDebugService private readonly debugService: IDebugService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IDialogService private readonly dialogService: IDialogService, + ) { + lifecycleService.onBeforeShutdown(async e => e.veto(this.shouldVetoShutdown(e.reason), 'veto.debug')); + } + + private shouldVetoShutdown(_reason: ShutdownReason): boolean | Promise { + const rootSessions = this.debugService.getModel().getSessions().filter(s => s.parentSession === undefined); + if (rootSessions.length === 0) { + return false; + } + + const shouldConfirmOnExit = this.configurationService.getValue('debug').confirmOnExit; + if (shouldConfirmOnExit === 'never') { + return false; + } + + return this._showWindowCloseConfirmation(rootSessions.length); + } + + protected async _showWindowCloseConfirmation(numSessions: number): Promise { + let message: string; + if (numSessions === 1) { + message = nls.localize('debug.debugSessionCloseConfirmationSingular', "There is an active debug session, are you sure you want to terminate it?"); + } else { + message = nls.localize('debug.debugSessionCloseConfirmationPlural', "There are active debug sessions, are you sure you want to terminate them?"); + } + const res = await this.dialogService.confirm({ + message, + type: 'warning', + }); + return !res.confirmed; + } +} diff --git a/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts b/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts index 1482614113e..232b2368df3 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts @@ -50,6 +50,7 @@ import { PANEL_BORDER } from 'vs/workbench/common/theme'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { peekViewBorder /*, peekViewEditorBackground, peekViewResultsBackground */ } from 'vs/editor/contrib/peekView/peekView'; import * as icons from 'vs/workbench/contrib/notebook/browser/notebookIcons'; +import { EditOperation } from 'vs/editor/common/core/editOperation'; Registry.as(EditorExtensions.EditorPane).registerEditorPane( @@ -452,9 +453,10 @@ registerAction2(class extends Action2 { if (editorControl && editorControl.notebookEditor && editorControl.codeEditor) { const notebookDocument = editorControl.notebookEditor.textModel; const textModel = editorControl.codeEditor.getModel(); + const range = editorControl.codeEditor.getModel()?.getFullModelRange(); - if (notebookDocument && textModel) { - textModel.setValue(''); + if (notebookDocument && textModel && range) { + editorControl.codeEditor.executeEdits('', [EditOperation.replace(range, null)]); } } } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts index afda964fdf2..e280c748885 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts @@ -71,6 +71,7 @@ const EXECUTE_CELL_INSERT_BELOW = 'notebook.cell.executeAndInsertBelow'; const EXECUTE_CELL_AND_BELOW = 'notebook.cell.executeCellAndBelow'; const EXECUTE_CELLS_ABOVE = 'notebook.cell.executeCellsAbove'; const CLEAR_CELL_OUTPUTS_COMMAND_ID = 'notebook.cell.clearOutputs'; +const TOGGLE_CELL_OUTPUTS_COMMAND_ID = 'notebook.cell.toggleOutputs'; const CENTER_ACTIVE_CELL = 'notebook.centerActiveCell'; const COLLAPSE_CELL_INPUT_COMMAND_ID = 'notebook.cell.collapseCellInput'; @@ -691,6 +692,46 @@ registerAction2(class CancelExecuteCell extends NotebookMultiCellAction { + constructor() { + super({ + id: TOGGLE_CELL_OUTPUTS_COMMAND_ID, + precondition: NOTEBOOK_CELL_LIST_FOCUSED, + title: localize('notebookActions.toggleOutputs', "Toggle Outputs"), + description: { + description: localize('notebookActions.toggleOutputs', "Toggle Outputs"), + args: cellExecutionArgs + } + }); + } + + parseArgs(accessor: ServicesAccessor, ...args: any[]): INotebookActionContext | undefined { + return parseMultiCellExecutionArgs(accessor, ...args); + } + + async runWithContext(accessor: ServicesAccessor, context: INotebookActionContext): Promise { + const textModel = context.notebookEditor.viewModel.notebookDocument; + let cells: ICellViewModel[] = []; + if (context.ui && context.cell) { + cells = [context.cell]; + } else if (context.selectedCells) { + cells = [...context.selectedCells]; + } else { + cells = [...context.notebookEditor.viewModel.getCells()]; + } + + const edits: ICellEditOperation[] = []; + for (const cell of cells) { + const index = textModel.cells.indexOf(cell.model); + if (index >= 0) { + edits.push({ editType: CellEditType.Metadata, index, metadata: { ...cell.metadata, outputCollapsed: !cell.metadata.outputCollapsed } }); + } + } + + textModel.applyEdits(edits, true, undefined, () => undefined, undefined); + } +}); + export class DeleteCellAction extends MenuItemAction { constructor( @IContextKeyService contextKeyService: IContextKeyService, 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 4dd7729e00a..1523d07d659 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/findModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/findModel.ts @@ -24,7 +24,7 @@ export class FindModel extends Disposable { private _currentMatch: number = -1; private _allMatchesDecorations: ICellModelDecorations[] = []; private _currentMatchDecorations: ICellModelDecorations[] = []; - private _modelDisposable = new DisposableStore(); + private readonly _modelDisposable = this._register(new DisposableStore()); get findMatches() { return this._findMatches; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel.ts b/src/vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel.ts index b409e7eb737..1674b57fc41 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from 'vs/base/common/event'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { TrackedRangeStickiness } from 'vs/editor/common/model'; import { FoldingRegion, FoldingRegions } from 'vs/editor/contrib/folding/foldingRanges'; import { IFoldingRangeData, sanitizeRanges } from 'vs/editor/contrib/folding/syntaxRangeProvider'; @@ -16,7 +16,7 @@ type RegionFilter = (r: FoldingRegion) => boolean; type RegionFilterWithLevel = (r: FoldingRegion, level: number) => boolean; -export class FoldingModel { +export class FoldingModel implements IDisposable { private _viewModel: NotebookViewModel | null = null; private readonly _viewModelStore = new DisposableStore(); private _regions: FoldingRegions; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts index fb3438d7024..037d8e4de4c 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts @@ -273,17 +273,15 @@ class NotebookComparator implements IOutlineComparator { } } -export class NotebookCellOutline implements IOutline { +export class NotebookCellOutline extends Disposable implements IOutline { - private readonly _dispoables = new DisposableStore(); - - private readonly _onDidChange = new Emitter(); + private readonly _onDidChange = this._register(new Emitter()); readonly onDidChange: Event = this._onDidChange.event; private _entries: OutlineEntry[] = []; private _activeEntry?: OutlineEntry; - private readonly _entriesDisposables = new DisposableStore(); + private readonly _entriesDisposables = this._register(new DisposableStore()); readonly config: IOutlineListConfig; readonly outlineKind = 'notebookCells'; @@ -301,8 +299,8 @@ export class NotebookCellOutline implements IOutline { @IMarkerService private readonly _markerService: IMarkerService, @IConfigurationService private readonly _configurationService: IConfigurationService, ) { - const selectionListener = new MutableDisposable(); - this._dispoables.add(selectionListener); + super(); + const selectionListener = this._register(new MutableDisposable()); const installSelectionListener = () => { if (!_editor.viewModel) { selectionListener.clear(); @@ -314,18 +312,18 @@ export class NotebookCellOutline implements IOutline { } }; - this._dispoables.add(_editor.onDidChangeModel(() => { + this._register(_editor.onDidChangeModel(() => { this._recomputeState(); installSelectionListener(); })); - this._dispoables.add(_configurationService.onDidChangeConfiguration(e => { + this._register(_configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('notebook.outline.showCodeCells')) { this._recomputeState(); } })); - this._dispoables.add(themeService.onDidFileIconThemeChange(() => { + this._register(themeService.onDidFileIconThemeChange(() => { this._onDidChange.fire({}); })); @@ -367,12 +365,6 @@ export class NotebookCellOutline implements IOutline { }; } - dispose(): void { - this._onDidChange.dispose(); - this._dispoables.dispose(); - this._entriesDisposables.dispose(); - } - private _recomputeState(): void { this._entriesDisposables.clear(); this._activeEntry = undefined; diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts index f9c4a0f0209..c111f70b961 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts @@ -302,10 +302,8 @@ abstract class AbstractElementRenderer extends Disposable { super(); // init this._isDisposed = false; - this._metadataEditorDisposeStore = new DisposableStore(); - this._outputEditorDisposeStore = new DisposableStore(); - this._register(this._metadataEditorDisposeStore); - this._register(this._outputEditorDisposeStore); + this._metadataEditorDisposeStore = this._register(new DisposableStore()); + this._outputEditorDisposeStore = this._register(new DisposableStore()); this._register(cell.onDidLayoutChange(e => this.layout(e))); this._register(cell.onDidLayoutChange(e => this.updateBorders())); this.init(); diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffElementOutputs.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffElementOutputs.ts index 3aef610061a..e6d0c482d5d 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffElementOutputs.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffElementOutputs.ts @@ -26,7 +26,7 @@ interface IMimeTypeRenderer extends IQuickPickItem { } export class OutputElement extends Disposable { - readonly resizeListener = new DisposableStore(); + readonly resizeListener = this._register(new DisposableStore()); domNode!: HTMLElement; renderResult?: IRenderOutput; diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts index 5a0073f9c92..f7201c98af7 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts @@ -34,9 +34,9 @@ interface ILayoutInfoDelta extends ILayoutInfoDelta0 { export abstract class DiffElementViewModelBase extends Disposable { public metadataFoldingState: PropertyFoldingState; public outputFoldingState: PropertyFoldingState; - protected _layoutInfoEmitter = new Emitter(); + protected _layoutInfoEmitter = this._register(new Emitter()); onDidLayoutChange = this._layoutInfoEmitter.event; - protected _stateChangeEmitter = new Emitter<{ renderOutput: boolean; }>(); + protected _stateChangeEmitter = this._register(new Emitter<{ renderOutput: boolean; }>()); onDidStateChange = this._stateChangeEmitter.event; protected _layoutInfo!: IDiffElementLayoutInfo; diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel.ts index 4adfd296184..1d2e892001c 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel.ts @@ -69,9 +69,9 @@ export class DiffNestedCellViewModel extends Disposable implements IDiffNestedCe protected _outputCollection: number[] = []; protected _outputsTop: PrefixSumComputer | null = null; - protected readonly _onDidChangeOutputLayout = new Emitter(); - readonly onDidChangeOutputLayout = this._onDidChangeOutputLayout.event; + protected readonly _onDidChangeOutputLayout = this._register(new Emitter()); + readonly onDidChangeOutputLayout = this._onDidChangeOutputLayout.event; constructor( readonly textModel: NotebookCellTextModel, diff --git a/src/vs/workbench/contrib/notebook/browser/diff/eventDispatcher.ts b/src/vs/workbench/contrib/notebook/browser/diff/eventDispatcher.ts index 2e4cb22eea4..d685d34cf06 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/eventDispatcher.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/eventDispatcher.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; import { IDiffElementLayoutInfo } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser'; import { NotebookLayoutChangeEvent, NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; @@ -32,14 +33,12 @@ export class NotebookCellLayoutChangedEvent { export type NotebookDiffViewEvent = NotebookDiffLayoutChangedEvent | NotebookCellLayoutChangedEvent; -export class NotebookDiffEditorEventDispatcher { - protected readonly _onDidChangeLayout = new Emitter(); +export class NotebookDiffEditorEventDispatcher extends Disposable { + protected readonly _onDidChangeLayout = this._register(new Emitter()); readonly onDidChangeLayout = this._onDidChangeLayout.event; - protected readonly _onDidChangeCellLayout = new Emitter(); - readonly onDidChangeCellLayout = this._onDidChangeCellLayout.event; - constructor() { - } + protected readonly _onDidChangeCellLayout = this._register(new Emitter()); + readonly onDidChangeCellLayout = this._onDidChangeCellLayout.event; emit(events: NotebookDiffViewEvent[]) { for (let i = 0, len = events.length; i < len; i++) { diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts index 23b0f56ee42..d7b999e881b 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts @@ -73,7 +73,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD private _revealFirst: boolean; private readonly _insetModifyQueueByOutputId = new SequencerByKey(); - protected _onDidDynamicOutputRendered = new Emitter<{ cell: IGenericCellViewModel, output: ICellOutputViewModel; }>(); + protected _onDidDynamicOutputRendered = this._register(new Emitter<{ cell: IGenericCellViewModel, output: ICellOutputViewModel; }>()); onDidDynamicOutputRendered = this._onDidDynamicOutputRendered.event; private _notebookOptions: NotebookOptions; diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 93e7a8a8caa..4a53c719346 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -91,6 +91,8 @@ import 'vs/workbench/contrib/notebook/browser/view/output/transforms/richTransfo import { editorOptionsRegistry } from 'vs/editor/common/config/editorOptions'; import { NotebookExecutionService } from 'vs/workbench/contrib/notebook/browser/notebookExecutionServiceImpl'; import { INotebookExecutionService } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; +import { INotebookKeymapService } from 'vs/workbench/contrib/notebook/common/notebookKeymapService'; +import { NotebookKeymapService } from 'vs/workbench/contrib/notebook/browser/notebookKeymapServiceImpl'; /*--------------------------------------------------------------------------------------------- */ @@ -607,6 +609,7 @@ registerSingleton(INotebookEditorService, NotebookEditorWidgetService, true); registerSingleton(INotebookKernelService, NotebookKernelService, true); registerSingleton(INotebookExecutionService, NotebookExecutionService, true); registerSingleton(INotebookRendererMessagingService, NotebookRendererMessagingService, true); +registerSingleton(INotebookKeymapService, NotebookKeymapService, true); const schemas: IJSONSchemaMap = {}; function isConfigurationPropertySchema(x: IConfigurationPropertySchema | { [path: string]: IConfigurationPropertySchema; }): x is IConfigurationPropertySchema { @@ -712,7 +715,7 @@ configurationRegistry.registerConfiguration({ tags: ['notebookLayout'] }, [FocusIndicator]: { - description: nls.localize('notebook.focusIndicator.description', "Control whether to render the focus indicator as a cell border or a highlight bar on the left gutter"), + description: nls.localize('notebook.focusIndicator.description', "Controls where the focus indicator is rendered, either along the cell borders or on the left gutter"), type: 'string', enum: ['border', 'gutter'], default: 'gutter', diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index 330538c2d3f..55b6327606f 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -92,9 +92,10 @@ export const EXPAND_CELL_OUTPUT_COMMAND_ID = 'notebook.cell.expandCellOutput'; // Hardcoding viewType/extension ID for now. TODO these should be replaced once we can // look them up in the marketplace dynamically. +export const IPYNB_VIEW_TYPE = 'jupyter-notebook'; export const JUPYTER_EXTENSION_ID = 'ms-toolsai.jupyter'; export const KERNEL_EXTENSIONS = new Map([ - ['jupyter-notebook', JUPYTER_EXTENSION_ID], + [IPYNB_VIEW_TYPE, JUPYTER_EXTENSION_ID], ]); //#endregion @@ -129,7 +130,7 @@ export interface IRenderOutputViaExtension { export type IInsetRenderOutput = IRenderPlainHtmlOutput | IRenderOutputViaExtension; export type IRenderOutput = IRenderMainframeOutput | IInsetRenderOutput; -export interface ICellOutputViewModel { +export interface ICellOutputViewModel extends IDisposable { cellViewModel: IGenericCellViewModel; /** * When rendering an output, `model` should always be used as we convert legacy `text/error` output to `display_data` output under the hood. @@ -751,8 +752,8 @@ export interface BaseCellRenderTemplate { betweenCellToolbar: ToolBar; focusIndicatorLeft: HTMLElement; focusIndicatorRight: HTMLElement; - disposables: DisposableStore; - elementDisposables: DisposableStore; + readonly disposables: DisposableStore; + readonly elementDisposables: DisposableStore; bottomCellContainer: HTMLElement; currentRenderedCell?: ICellViewModel; statusBar: CellEditorStatusBar; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookCellStatusBarServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookCellStatusBarServiceImpl.ts index cb27b64cd42..56cf615dd92 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookCellStatusBarServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookCellStatusBarServiceImpl.ts @@ -13,10 +13,10 @@ import { INotebookCellStatusBarItemList, INotebookCellStatusBarItemProvider } fr export class NotebookCellStatusBarService extends Disposable implements INotebookCellStatusBarService { - private _onDidChangeProviders = new Emitter(); + private _onDidChangeProviders = this._register(new Emitter()); readonly onDidChangeProviders: Event = this._onDidChangeProviders.event; - private _onDidChangeItems = new Emitter(); + private _onDidChangeItems = this._register(new Emitter()); readonly onDidChangeItems: Event = this._onDidChangeItems.event; private _providers: INotebookCellStatusBarItemProvider[] = []; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index e4132081efe..a0c363ca2fb 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -2469,7 +2469,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor //#region --- webview IPC ---- - private readonly _onDidReceiveMessage = new Emitter(); + private readonly _onDidReceiveMessage = this._register(new Emitter()); readonly onDidReceiveMessage: Event = this._onDidReceiveMessage.event; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookExecutionServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookExecutionServiceImpl.ts index 643eb717044..6bedd79feb1 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookExecutionServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookExecutionServiceImpl.ts @@ -89,7 +89,8 @@ class CellExecution implements INotebookCellExecution, IDisposable { editType: CellEditType.PartialInternalMetadata, handle: cellHandle, internalMetadata: { - runState: NotebookCellExecutionState.Pending + runState: NotebookCellExecutionState.Pending, + executionOrder: null } }; this._applyExecutionEdits([startExecuteEdit]); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookKernelServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookKernelServiceImpl.ts index e084aa87432..e0071988e6a 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookKernelServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookKernelServiceImpl.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Event, Emitter } from 'vs/base/common/event'; -import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { INotebookTextModel } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookKernel, ISelectedNotebooksChangeEvent, INotebookKernelMatchResult, INotebookKernelService, INotebookTextModelLike } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { LRUCache, ResourceMap } from 'vs/base/common/map'; @@ -43,20 +43,19 @@ class NotebookTextModelLikeId { } } -export class NotebookKernelService implements INotebookKernelService { +export class NotebookKernelService extends Disposable implements INotebookKernelService { declare _serviceBrand: undefined; - private readonly _disposables = new DisposableStore(); private readonly _kernels = new Map(); private readonly _typeBindings = new LRUCache(100, 0.7); private readonly _notebookBindings = new LRUCache(1000, 0.7); - private readonly _onDidChangeNotebookKernelBinding = new Emitter(); - private readonly _onDidAddKernel = new Emitter(); - private readonly _onDidRemoveKernel = new Emitter(); - private readonly _onDidChangeNotebookAffinity = new Emitter(); + private readonly _onDidChangeNotebookKernelBinding = this._register(new Emitter()); + private readonly _onDidAddKernel = this._register(new Emitter()); + private readonly _onDidRemoveKernel = this._register(new Emitter()); + private readonly _onDidChangeNotebookAffinity = this._register(new Emitter()); readonly onDidChangeSelectedNotebooks: Event = this._onDidChangeNotebookKernelBinding.event; readonly onDidAddKernel: Event = this._onDidAddKernel.event; @@ -70,11 +69,12 @@ export class NotebookKernelService implements INotebookKernelService { @INotebookService private readonly _notebookService: INotebookService, @IStorageService private readonly _storageService: IStorageService, ) { + super(); // auto associate kernels to new notebook documents, also emit event when // a notebook has been closed (but don't update the memento) - this._disposables.add(_notebookService.onDidAddNotebookDocument(this._tryAutoBindNotebook, this)); - this._disposables.add(_notebookService.onWillRemoveNotebookDocument(notebook => { + this._register(_notebookService.onDidAddNotebookDocument(this._tryAutoBindNotebook, this)); + this._register(_notebookService.onWillRemoveNotebookDocument(notebook => { const kernelId = this._notebookBindings.get(NotebookTextModelLikeId.str(notebook)); if (kernelId) { this._onDidChangeNotebookKernelBinding.fire({ notebook: notebook.uri, oldKernel: kernelId, newKernel: undefined }); @@ -96,12 +96,9 @@ export class NotebookKernelService implements INotebookKernelService { } } - dispose() { - this._disposables.dispose(); - this._onDidChangeNotebookKernelBinding.dispose(); - this._onDidAddKernel.dispose(); - this._onDidRemoveKernel.dispose(); + override dispose() { this._kernels.clear(); + super.dispose(); } private _persistSoonHandle?: IDisposable; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookKeymapServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookKeymapServiceImpl.ts new file mode 100644 index 00000000000..dd54f520c31 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/notebookKeymapServiceImpl.ts @@ -0,0 +1,118 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { onUnexpectedError } from 'vs/base/common/errors'; +import { Event } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { localize } from 'vs/nls'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { getInstalledExtensions, IExtensionStatus } from 'vs/workbench/contrib/extensions/common/extensionsUtils'; +import { INotebookKeymapService } from 'vs/workbench/contrib/notebook/common/notebookKeymapService'; +import { EnablementState, IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { IExtensionIdentifier, IExtensionManagementService, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { Memento, MementoObject } from 'vs/workbench/common/memento'; + +function onExtensionChanged(accessor: ServicesAccessor): Event { + const extensionService = accessor.get(IExtensionManagementService); + const extensionEnablementService = accessor.get(IWorkbenchExtensionEnablementService); + const onDidInstallExtensions = Event.chain(extensionService.onDidInstallExtensions) + .filter(e => e.some(({ operation }) => operation === InstallOperation.Install)) + .map(e => e.map(({ identifier }) => identifier)) + .event; + return Event.debounce(Event.any( + Event.chain(Event.any(onDidInstallExtensions, Event.map(extensionService.onDidUninstallExtension, e => [e.identifier]))) + .event, + Event.map(extensionEnablementService.onEnablementChanged, extensions => extensions.map(e => e.identifier)) + ), (result: IExtensionIdentifier[] | undefined, identifiers: IExtensionIdentifier[]) => { + result = result || (identifiers.length ? [identifiers[0]] : []); + for (const identifier of identifiers) { + if (result.some(l => !areSameExtensions(l, identifier))) { + result.push(identifier); + } + } + + return result; + }); +} + +const hasRecommendedKeymapKey = 'hasRecommendedKeymap'; + +export class NotebookKeymapService extends Disposable implements INotebookKeymapService { + _serviceBrand: undefined; + + private notebookKeymapMemento: Memento; + private notebookKeymap: MementoObject; + + constructor( + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, + @INotificationService private readonly notificationService: INotificationService, + @IStorageService storageService: IStorageService, + @ILifecycleService lifecycleService: ILifecycleService, + ) { + super(); + + this.notebookKeymapMemento = new Memento('notebookKeymap', storageService); + this.notebookKeymap = this.notebookKeymapMemento.getMemento(StorageScope.GLOBAL, StorageTarget.USER); + + this._register(lifecycleService.onDidShutdown(() => this.dispose())); + this._register(this.instantiationService.invokeFunction(onExtensionChanged)((identifiers => { + Promise.all(identifiers.map(identifier => this.checkForOtherKeymaps(identifier))) + .then(undefined, onUnexpectedError); + }))); + } + + private checkForOtherKeymaps(extensionIdentifier: IExtensionIdentifier): Promise { + return this.instantiationService.invokeFunction(getInstalledExtensions).then(extensions => { + const keymaps = extensions.filter(extension => isNotebookKeymapExtension(extension)); + const extension = keymaps.find(extension => areSameExtensions(extension.identifier, extensionIdentifier)); + if (extension && extension.globallyEnabled) { + // there is already a keymap extension + this.notebookKeymap[hasRecommendedKeymapKey] = true; + this.notebookKeymapMemento.saveMemento(); + const otherKeymaps = keymaps.filter(extension => !areSameExtensions(extension.identifier, extensionIdentifier) && extension.globallyEnabled); + if (otherKeymaps.length) { + return this.promptForDisablingOtherKeymaps(extension, otherKeymaps); + } + } + return undefined; + }); + } + + private promptForDisablingOtherKeymaps(newKeymap: IExtensionStatus, oldKeymaps: IExtensionStatus[]): void { + const onPrompt = (confirmed: boolean) => { + if (confirmed) { + this.extensionEnablementService.setEnablement(oldKeymaps.map(keymap => keymap.local), EnablementState.DisabledGlobally); + } + }; + + this.notificationService.prompt(Severity.Info, localize('disableOtherKeymapsConfirmation', "Disable other keymaps ({0}) to avoid conflicts between keybindings?", oldKeymaps.map(k => `'${k.local.manifest.displayName}'`).join(', ')), + [{ + label: localize('yes', "Yes"), + run: () => onPrompt(true) + }, { + label: localize('no', "No"), + run: () => onPrompt(false) + }] + ); + } +} + +export function isNotebookKeymapExtension(extension: IExtensionStatus): boolean { + if (extension.local.manifest.extensionPack) { + return false; + } + + const keywords = extension.local.manifest.keywords; + if (!keywords) { + return false; + } + + return keywords.indexOf('notebook-keymap') !== -1; +} diff --git a/src/vs/workbench/contrib/notebook/browser/notebookRendererMessagingServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookRendererMessagingServiceImpl.ts index e94a907f93a..05ce804d229 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookRendererMessagingServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookRendererMessagingServiceImpl.ts @@ -4,12 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; import { INotebookRendererMessagingService, IScopedRendererMessaging } from 'vs/workbench/contrib/notebook/common/notebookRendererMessagingService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; type MessageToSend = { editorId: string; rendererId: string; message: unknown }; -export class NotebookRendererMessagingService implements INotebookRendererMessagingService { +export class NotebookRendererMessagingService extends Disposable implements INotebookRendererMessagingService { declare _serviceBrand: undefined; /** * Activation promises. Maps renderer IDs to a queue of messages that should @@ -17,10 +18,14 @@ export class NotebookRendererMessagingService implements INotebookRendererMessag */ private readonly activations = new Map(); private readonly scopedMessaging = new Map(); - private readonly postMessageEmitter = new Emitter(); + private readonly postMessageEmitter = this._register(new Emitter()); public readonly onShouldPostMessage = this.postMessageEmitter.event; - constructor(@IExtensionService private readonly extensionService: IExtensionService) { } + constructor( + @IExtensionService private readonly extensionService: IExtensionService + ) { + super(); + } /** @inheritdoc */ public receiveMessage(editorId: string | undefined, rendererId: string, message: unknown): Promise { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts index 6c99d3319bc..5b9c153fa0a 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts @@ -49,7 +49,7 @@ export class NotebookProviderInfoStore extends Disposable { private _handled: boolean = false; private readonly _contributedEditors = new Map(); - private readonly _contributedEditorDisposables = new DisposableStore(); + private readonly _contributedEditorDisposables = this._register(new DisposableStore()); constructor( @IStorageService storageService: IStorageService, diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index f6b9d46de61..a530ff97eff 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -8,7 +8,7 @@ import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; import { IListRenderer, IListVirtualDelegate, ListError } from 'vs/base/browser/ui/list/list'; import { IListStyles, IStyleController } from 'vs/base/browser/ui/list/listWidget'; import { Emitter, Event } from 'vs/base/common/event'; -import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { isMacintosh } from 'vs/base/common/platform'; import { ScrollEvent } from 'vs/base/common/scrollable'; import { Range } from 'vs/editor/common/core/range'; @@ -43,17 +43,17 @@ export class NotebookCellList extends WorkbenchList implements ID return this.view.containerDomNode; } private _previousFocusedElements: CellViewModel[] = []; - private _localDisposableStore = new DisposableStore(); - private _viewModelStore = new DisposableStore(); + private readonly _localDisposableStore = new DisposableStore(); + private readonly _viewModelStore = new DisposableStore(); private styleElement?: HTMLStyleElement; - private readonly _onDidRemoveOutputs = new Emitter(); + private readonly _onDidRemoveOutputs = this._localDisposableStore.add(new Emitter()); readonly onDidRemoveOutputs = this._onDidRemoveOutputs.event; - private readonly _onDidHideOutputs = new Emitter(); + private readonly _onDidHideOutputs = this._localDisposableStore.add(new Emitter()); readonly onDidHideOutputs = this._onDidHideOutputs.event; - private readonly _onDidRemoveCellsFromView = new Emitter(); + private readonly _onDidRemoveCellsFromView = this._localDisposableStore.add(new Emitter()); readonly onDidRemoveCellsFromView = this._onDidRemoveCellsFromView.event; private _viewModel: NotebookViewModel | null = null; @@ -63,7 +63,7 @@ export class NotebookCellList extends WorkbenchList implements ID private _hiddenRangeIds: string[] = []; private hiddenRangesPrefixSum: PrefixSumComputer | null = null; - private readonly _onDidChangeVisibleRanges = new Emitter(); + private readonly _onDidChangeVisibleRanges = this._localDisposableStore.add(new Emitter()); onDidChangeVisibleRanges: Event = this._onDidChangeVisibleRanges.event; private _visibleRanges: ICellRange[] = []; @@ -128,8 +128,8 @@ export class NotebookCellList extends WorkbenchList implements ID const notebookEditorCursorAtBoundaryContext = NOTEBOOK_EDITOR_CURSOR_BOUNDARY.bindTo(contextKeyService); notebookEditorCursorAtBoundaryContext.set('none'); - let cursorSelectionListener: IDisposable | null = null; - let textEditorAttachListener: IDisposable | null = null; + const cursorSelectionListener = this._localDisposableStore.add(new MutableDisposable()); + const textEditorAttachListener = this._localDisposableStore.add(new MutableDisposable()); const recomputeContext = (element: CellViewModel) => { switch (element.cursorAtBoundary()) { @@ -153,18 +153,16 @@ export class NotebookCellList extends WorkbenchList implements ID // Cursor Boundary context this._localDisposableStore.add(this.onDidChangeFocus((e) => { if (e.elements.length) { - cursorSelectionListener?.dispose(); - textEditorAttachListener?.dispose(); // we only validate the first focused element const focusedElement = e.elements[0]; - cursorSelectionListener = focusedElement.onDidChangeState((e) => { + cursorSelectionListener.value = focusedElement.onDidChangeState((e) => { if (e.selectionChanged) { recomputeContext(focusedElement); } }); - textEditorAttachListener = focusedElement.onDidChangeEditorAttachState(() => { + textEditorAttachListener.value = focusedElement.onDidChangeEditorAttachState(() => { if (focusedElement.editorAttached) { recomputeContext(focusedElement); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform.ts b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform.ts index 7e671001e4a..cad4d84642a 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform.ts @@ -79,7 +79,7 @@ class StreamRendererContrib extends Disposable implements IOutputRendererContrib truncatedArrayOfString(notebookUri, output.cellViewModel, Math.max(lineLimit, 6), contentNode, [text], disposables, linkDetector, this.openerService, this.themeService); container.appendChild(contentNode); - return { type: RenderOutputType.Mainframe }; + return { type: RenderOutputType.Mainframe, disposable: disposables }; } } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellDnd.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellDnd.ts index 7a98d92b188..40642ce6329 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellDnd.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellDnd.ts @@ -5,7 +5,7 @@ import * as DOM from 'vs/base/browser/dom'; import { Delayer } from 'vs/base/common/async'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import * as platform from 'vs/base/common/platform'; import { BaseCellRenderTemplate, expandCellRangesWithHiddenCells, ICellViewModel, INotebookCellList, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { cloneNotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; @@ -37,7 +37,9 @@ export class CellDragAndDropController extends Disposable { private list!: INotebookCellList; private isScrolling = false; - private scrollingDelayer: Delayer; + private readonly scrollingDelayer: Delayer; + + private readonly listOnWillScrollListener = this._register(new MutableDisposable()); constructor( private readonly notebookEditor: INotebookEditor, @@ -75,13 +77,13 @@ export class CellDragAndDropController extends Disposable { this.onCellDragLeave(event); }); - this.scrollingDelayer = new Delayer(200); + this.scrollingDelayer = this._register(new Delayer(200)); } setList(value: INotebookCellList) { this.list = value; - this.list.onWillScroll(e => { + this.listOnWillScrollListener.value = this.list.onWillScroll(e => { if (!e.scrollTopChanged) { return; } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellEditorOptions.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellEditorOptions.ts index e1dacccee5b..ac6cb6df302 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellEditorOptions.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellEditorOptions.ts @@ -46,7 +46,7 @@ export class CellEditorOptions extends Disposable { private _value: IEditorOptions; private _lineNumbers: 'on' | 'off' | 'inherit' = 'inherit'; - private readonly _onDidChange = new Emitter(); + private readonly _onDidChange = this._register(new Emitter()); readonly onDidChange: Event = this._onDidChange.event; private _localDisposableStore = this._register(new DisposableStore()); @@ -114,11 +114,6 @@ export class CellEditorOptions extends Disposable { return computed; } - override dispose(): void { - this._onDidChange.dispose(); - super.dispose(); - } - getValue(internalMetadata?: NotebookCellInternalMetadata): IEditorOptions { return { ...this._value, diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellOutput.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellOutput.ts index 463e4aaba20..c6f60e48668 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellOutput.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellOutput.ts @@ -514,7 +514,7 @@ export class CellOutputContainer extends Disposable { constructor( private notebookEditor: INotebookEditor, private viewCell: CodeCellViewModel, - private templateData: CodeCellRenderTemplate, + private readonly templateData: CodeCellRenderTemplate, private options: { limit: number; }, @IOpenerService private readonly openerService: IOpenerService, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -568,7 +568,7 @@ export class CellOutputContainer extends Disposable { this.templateData.outputShowMoreContainer.innerText = ''; if (this.viewCell.outputsViewModels.length > this.options.limit) { - this.templateData.outputShowMoreContainer.appendChild(this._generateShowMoreElement()); + this.templateData.outputShowMoreContainer.appendChild(this._generateShowMoreElement(this.templateData.disposables)); } else { DOM.hide(this.templateData.outputShowMoreContainer); this.viewCell.updateOutputShowMoreContainerHeight(0); @@ -773,7 +773,7 @@ export class CellOutputContainer extends Disposable { if (this.viewCell.outputsViewModels.length > this.options.limit) { DOM.show(this.templateData.outputShowMoreContainer); if (!this.templateData.outputShowMoreContainer.hasChildNodes()) { - this.templateData.outputShowMoreContainer.appendChild(this._generateShowMoreElement()); + this.templateData.outputShowMoreContainer.appendChild(this._generateShowMoreElement(this.templateData.disposables)); } this.viewCell.updateOutputShowMoreContainerHeight(46); } else { @@ -789,7 +789,7 @@ export class CellOutputContainer extends Disposable { this._validateFinalOutputHeight(!outputHasDynamicHeight || this.viewCell.outputsViewModels.length === 0); } - private _generateShowMoreElement(): any { + private _generateShowMoreElement(disposables: DisposableStore): HTMLElement { const md: IMarkdownString = { value: `There are more than ${this.options.limit} outputs, [show more (open the raw output data in a text editor) ...](command:workbench.action.openLargeOutput)`, isTrusted: true, @@ -805,7 +805,7 @@ export class CellOutputContainer extends Disposable { return; }, - disposables: new DisposableStore() + disposables } }); 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 ba77cce9fe9..0f3728e9959 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -1080,7 +1080,7 @@ export class ListTopCellToolbar extends Disposable { private topCellToolbar: HTMLElement; private menu: IMenu; private toolbar: ToolBar; - private _modelDisposables = new DisposableStore(); + private readonly _modelDisposables = this._register(new DisposableStore()); constructor( protected readonly notebookEditor: INotebookEditor, diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts index 750038b8b25..88e1712dc90 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts @@ -31,9 +31,9 @@ export class CodeCell extends Disposable { private _renderedOutputCollapseState: boolean | undefined; constructor( - private notebookEditor: IActiveNotebookEditor, - private viewCell: CodeCellViewModel, - private templateData: CodeCellRenderTemplate, + private readonly notebookEditor: IActiveNotebookEditor, + private readonly viewCell: CodeCellViewModel, + private readonly templateData: CodeCellRenderTemplate, @IInstantiationService private readonly instantiationService: IInstantiationService, @INotebookCellStatusBarService readonly notebookCellStatusBarService: INotebookCellStatusBarService, @IKeybindingService readonly keybindingService: IKeybindingService, diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts index 69b9d893d3a..75ba935b276 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -455,12 +455,6 @@ async function webviewPreloads(ctx: PreloadContext) { interface IOutputItem { readonly id: string; - /** @deprecated */ - readonly outputId?: string; - - /** @deprecated */ - readonly element: HTMLElement; - readonly mime: string; metadata: unknown; @@ -468,8 +462,34 @@ async function webviewPreloads(ctx: PreloadContext) { json(): any; data(): Uint8Array; blob(): Blob; - /** @deprecated */ - bytes(): Uint8Array; + } + + class OutputItem implements IOutputItem { + constructor( + public readonly id: string, + public readonly element: HTMLElement, + public readonly mime: string, + public readonly metadata: unknown, + public readonly valueBytes: Uint8Array + ) { } + + data() { + return this.valueBytes; + } + + bytes() { return this.data(); } + + text() { + return new TextDecoder().decode(this.valueBytes); + } + + json() { + return JSON.parse(this.text()); + } + + blob() { + return new Blob([this.valueBytes], { type: this.mime }); + } } const onDidReceiveKernelMessage = createEmitter(); @@ -555,25 +575,7 @@ async function webviewPreloads(ctx: PreloadContext) { } else { const rendererApi = preloadsAndErrors[0] as RendererApi; try { - rendererApi.renderOutputItem({ - id: outputId, - element: outputNode, - mime: content.mimeType, - metadata: content.metadata, - data() { - return content.valueBytes; - }, - bytes() { return this.data(); }, - text() { - return new TextDecoder().decode(content.valueBytes); - }, - json() { - return JSON.parse(this.text()); - }, - blob() { - return new Blob([content.valueBytes], { type: content.mimeType }); - } - }, outputNode); + rendererApi.renderOutputItem(new OutputItem(outputId, outputNode, content.mimeType, content.mimeType, content.valueBytes), outputNode); } catch (e) { showPreloadErrors(outputNode, e); } @@ -956,7 +958,7 @@ async function webviewPreloads(ctx: PreloadContext) { public deleteMarkupCell(id: string) { const cell = this.getExpectedMarkupCell(id); if (cell) { - cell.element.remove(); + cell.remove(); this._markupCells.delete(id); } } @@ -1066,6 +1068,8 @@ async function webviewPreloads(ctx: PreloadContext) { public readonly ready: Promise; + public readonly element: HTMLElement; + /// Internal field that holds text content private _content: string; @@ -1097,12 +1101,8 @@ async function webviewPreloads(ctx: PreloadContext) { //#region IOutputItem public readonly id: string; - public readonly mime; - public readonly element: HTMLElement; - - // deprecated fields + public readonly mime: string; public readonly metadata = undefined; - public readonly outputId?: string | undefined; text() { return this._content; } json() { return undefined; } @@ -1230,6 +1230,10 @@ async function webviewPreloads(ctx: PreloadContext) { this.updateContentAndRender(this._content); } + public remove() { + this.element.remove(); + } + private async updateMarkupDimensions() { dimensionUpdater.updateHeight(this.id, this.element.offsetHeight, { isOutput: false diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts index 629c790293e..6be6cc90e1f 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts @@ -5,28 +5,28 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable, IReference } from 'vs/base/common/lifecycle'; +import { Mimes } from 'vs/base/common/mime'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { IPosition } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; -import { IPosition } from 'vs/editor/common/core/position'; import * as editorCommon from 'vs/editor/common/editorCommon'; import * as model from 'vs/editor/common/model'; import { SearchParams } from 'vs/editor/common/model/textModelSearch'; -import { CellEditState, CellFocusMode, CursorAtBoundary, CellViewModelStateChangeEvent, IEditableCellViewModel, INotebookCellDecorationOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { CellKind, INotebookSearchOptions, INotebookCellStatusBarItem } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; -import { ViewContext } from 'vs/workbench/contrib/notebook/browser/viewModel/viewContext'; -import { Mimes } from 'vs/base/common/mime'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; +import { CellEditState, CellFocusMode, CellViewModelStateChangeEvent, CursorAtBoundary, IEditableCellViewModel, INotebookCellDecorationOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { ViewContext } from 'vs/workbench/contrib/notebook/browser/viewModel/viewContext'; +import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; +import { CellKind, INotebookCellStatusBarItem, INotebookSearchOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; export abstract class BaseCellViewModel extends Disposable { - protected readonly _onDidChangeEditorAttachState = new Emitter(); + protected readonly _onDidChangeEditorAttachState = this._register(new Emitter()); // Do not merge this event with `onDidChangeState` as we are using `Event.once(onDidChangeEditorAttachState)` elsewhere. readonly onDidChangeEditorAttachState = this._onDidChangeEditorAttachState.event; - protected readonly _onDidChangeState: Emitter = this._register(new Emitter()); + protected readonly _onDidChangeState = this._register(new Emitter()); public readonly onDidChangeState: Event = this._onDidChangeState.event; get handle() { @@ -109,11 +109,13 @@ export abstract class BaseCellViewModel extends Disposable { get editorAttached(): boolean { return !!this._textEditor; } - private _cursorChangeListener: IDisposable | null = null; + private _editorListeners: IDisposable[] = []; private _editorViewStates: editorCommon.ICodeEditorViewState | null = null; private _resolvedCellDecorations = new Map(); - private _cellDecorationsChanged = new Emitter<{ added: INotebookCellDecorationOptions[], removed: INotebookCellDecorationOptions[] }>(); + + private readonly _cellDecorationsChanged = this._register(new Emitter<{ added: INotebookCellDecorationOptions[], removed: INotebookCellDecorationOptions[] }>()); onCellDecorationsChanged: Event<{ added: INotebookCellDecorationOptions[], removed: INotebookCellDecorationOptions[] }> = this._cellDecorationsChanged.event; + private _resolvedDecorations = new Map(); - private _onDidChangeCellStatusBarItems = new Emitter(); + private readonly _onDidChangeCellStatusBarItems = this._register(new Emitter()); readonly onDidChangeCellStatusBarItems: Event = this._onDidChangeCellStatusBarItems.event; private _lastStatusBarId: number = 0; @@ -151,7 +153,8 @@ export abstract class BaseCellViewModel extends Disposable { private readonly _viewContext: ViewContext, private readonly _configurationService: IConfigurationService, private readonly _modelService: ITextModelService, - private readonly _undoRedoService: IUndoRedoService + private readonly _undoRedoService: IUndoRedoService, + // private readonly _keymapService: INotebookKeymapService ) { super(); @@ -189,14 +192,21 @@ export abstract class BaseCellViewModel extends Disposable { return false; } + // private handleKeyDown(e: IKeyboardEvent) { + // if (this.viewType === IPYNB_VIEW_TYPE && isWindows && e.ctrlKey && e.keyCode === KeyCode.Enter) { + // this._keymapService.promptKeymapRecommendation(); + // } + // } + attachTextEditor(editor: ICodeEditor) { if (!editor.hasModel()) { throw new Error('Invalid editor: model is missing'); } if (this._textEditor === editor) { - if (this._cursorChangeListener === null) { - this._cursorChangeListener = this._textEditor.onDidChangeCursorSelection(() => { this._onDidChangeState.fire({ selectionChanged: true }); }); + if (this._editorListeners.length === 0) { + this._editorListeners.push(this._textEditor.onDidChangeCursorSelection(() => { this._onDidChangeState.fire({ selectionChanged: true }); })); + // this._editorListeners.push(this._textEditor.onKeyDown(e => this.handleKeyDown(e))); this._onDidChangeState.fire({ selectionChanged: true }); } return; @@ -220,7 +230,8 @@ export abstract class BaseCellViewModel extends Disposable { } }); - this._cursorChangeListener = this._textEditor.onDidChangeCursorSelection(() => { this._onDidChangeState.fire({ selectionChanged: true }); }); + this._editorListeners.push(this._textEditor.onDidChangeCursorSelection(() => { this._onDidChangeState.fire({ selectionChanged: true }); })); + // this._editorListeners.push(this._textEditor.onKeyDown(e => this.handleKeyDown(e))); this._onDidChangeState.fire({ selectionChanged: true }); this._onDidChangeEditorAttachState.fire(); } @@ -237,8 +248,8 @@ export abstract class BaseCellViewModel extends Disposable { }); this._textEditor = undefined; - this._cursorChangeListener?.dispose(); - this._cursorChangeListener = null; + this._editorListeners.forEach(e => e.dispose()); + this._editorListeners = []; this._onDidChangeEditorAttachState.fire(); if (this._textModelRef) { @@ -540,6 +551,7 @@ export abstract class BaseCellViewModel extends Disposable { override dispose() { super.dispose(); + this._editorListeners.forEach(e => e.dispose()); this._undoRedoService.removeElements(this.uri); if (this._textModelRef) { diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts index a2d7ddcf17c..63f29f7dd57 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from 'vs/base/common/event'; +import { dispose } from 'vs/base/common/lifecycle'; import * as UUID from 'vs/base/common/uuid'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; @@ -15,6 +16,7 @@ import { CellOutputViewModel } from 'vs/workbench/contrib/notebook/browser/viewM import { ViewContext } from 'vs/workbench/contrib/notebook/browser/viewModel/viewContext'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { CellKind, INotebookSearchOptions, NotebookCellOutputsSplice } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookKeymapService } from 'vs/workbench/contrib/notebook/common/notebookKeymapService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { BaseCellViewModel } from './baseCellViewModel'; @@ -104,7 +106,8 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod @IConfigurationService configurationService: IConfigurationService, @INotebookService private readonly _notebookService: INotebookService, @ITextModelService modelService: ITextModelService, - @IUndoRedoService undoRedoService: IUndoRedoService + @IUndoRedoService undoRedoService: IUndoRedoService, + @INotebookKeymapService keymapService: INotebookKeymapService ) { super(viewType, model, UUID.generateUuid(), viewContext, configurationService, modelService, undoRedoService); this._outputViewModels = this.model.outputs.map(output => new CellOutputViewModel(this, output, this._notebookService)); @@ -118,6 +121,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod this._onDidChangeOutputs.fire(splice); this._onDidRemoveOutputs.fire(removedOutputs); this.layoutChange({ outputHeight: true }, 'CodeCellViewModel#model.onDidChangeOutputs'); + dispose(removedOutputs); })); this._register(this.model.onDidChangeMetadata(e => { @@ -424,5 +428,6 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod this._outputCollection = []; this._outputsTop = null; + dispose(this._outputViewModels); } } diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher.ts index c20c60d81a0..1ef72c90392 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher.ts @@ -6,6 +6,7 @@ import { Emitter } from 'vs/base/common/event'; import { NotebookDocumentMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookLayoutChangeEvent, NotebookLayoutInfo, CellViewModelStateChangeEvent, ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { Disposable } from 'vs/base/common/lifecycle'; export enum NotebookViewEventType { LayoutChanged = 1, @@ -41,16 +42,15 @@ export class NotebookCellStateChangedEvent { export type NotebookViewEvent = NotebookLayoutChangedEvent | NotebookMetadataChangedEvent | NotebookCellStateChangedEvent; -export class NotebookEventDispatcher { - protected readonly _onDidChangeLayout = new Emitter(); +export class NotebookEventDispatcher extends Disposable { + private readonly _onDidChangeLayout = this._register(new Emitter()); readonly onDidChangeLayout = this._onDidChangeLayout.event; - protected readonly _onDidChangeMetadata = new Emitter(); - readonly onDidChangeMetadata = this._onDidChangeMetadata.event; - protected readonly _onDidChangeCellState = new Emitter(); - readonly onDidChangeCellState = this._onDidChangeCellState.event; - constructor() { - } + private readonly _onDidChangeMetadata = this._register(new Emitter()); + readonly onDidChangeMetadata = this._onDidChangeMetadata.event; + + private readonly _onDidChangeCellState = this._register(new Emitter()); + readonly onDidChangeCellState = this._onDidChangeCellState.event; emit(events: NotebookViewEvent[]) { for (let i = 0, len = events.length; i < len; i++) { diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts index 602f95d17e6..3f1a6c39132 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts @@ -64,7 +64,7 @@ export class MarkupCellViewModel extends BaseCellViewModel implements ICellViewM throw new Error('MarkdownCellViewModel.editorHeight is write only'); } - protected readonly _onDidChangeLayout = new Emitter(); + protected readonly _onDidChangeLayout = this._register(new Emitter()); readonly onDidChangeLayout = this._onDidChangeLayout.event; get foldingState() { @@ -103,7 +103,7 @@ export class MarkupCellViewModel extends BaseCellViewModel implements ICellViewM return this.model.getHashValue(); } - private readonly _onDidHideInput = new Emitter(); + private readonly _onDidHideInput = this._register(new Emitter()); readonly onDidHideInput = this._onDidHideInput.event; constructor( @@ -115,7 +115,7 @@ export class MarkupCellViewModel extends BaseCellViewModel implements ICellViewM @IConfigurationService configurationService: IConfigurationService, @ITextModelService textModelService: ITextModelService, @IInstantiationService instantiationService: IInstantiationService, - @IUndoRedoService undoRedoService: IUndoRedoService + @IUndoRedoService undoRedoService: IUndoRedoService, ) { super(viewType, model, UUID.generateUuid(), viewContext, configurationService, textModelService, undoRedoService); diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts index 7ad0ff0d537..f4c146e40c9 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts @@ -18,19 +18,19 @@ import { NotebookCellOutputTextModel } from 'vs/workbench/contrib/notebook/commo import { CellInternalMetadataChangedEvent, CellKind, ICell, ICellOutput, IOutputDto, NotebookCellInternalMetadata, NotebookCellMetadata, NotebookCellOutputsSplice, TransientOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; export class NotebookCellTextModel extends Disposable implements ICell { - private _onDidChangeOutputs = new Emitter(); + private readonly _onDidChangeOutputs = this._register(new Emitter()); onDidChangeOutputs: Event = this._onDidChangeOutputs.event; - private _onDidChangeContent = new Emitter<'content' | 'language' | 'mime'>(); + private readonly _onDidChangeContent = this._register(new Emitter<'content' | 'language' | 'mime'>()); onDidChangeContent: Event<'content' | 'language' | 'mime'> = this._onDidChangeContent.event; - private _onDidChangeMetadata = new Emitter(); + private readonly _onDidChangeMetadata = this._register(new Emitter()); onDidChangeMetadata: Event = this._onDidChangeMetadata.event; - private _onDidChangeInternalMetadata = new Emitter(); + private readonly _onDidChangeInternalMetadata = this._register(new Emitter()); onDidChangeInternalMetadata: Event = this._onDidChangeInternalMetadata.event; - private _onDidChangeLanguage = new Emitter(); + private readonly _onDidChangeLanguage = this._register(new Emitter()); onDidChangeLanguage: Event = this._onDidChangeLanguage.event; private _outputs: NotebookCellOutputTextModel[]; @@ -134,7 +134,7 @@ export class NotebookCellTextModel extends Disposable implements ICell { return this._alternativeId; } - private _textModelDisposables = new DisposableStore(); + private readonly _textModelDisposables = this._register(new DisposableStore()); private _textModel: TextModel | undefined = undefined; get textModel(): TextModel | undefined { return this._textModel; diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts index 3610bfdc906..0ad9559b14d 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts @@ -166,7 +166,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel readonly onWillAddRemoveCells = this._onWillAddRemoveCells.event; readonly onDidChangeContent = this._onDidChangeContent.event; private _cellhandlePool: number = 0; - private _cellListeners: Map = new Map(); + private readonly _cellListeners: Map = new Map(); private _cells: NotebookCellTextModel[] = []; metadata: NotebookDocumentMetadata = {}; @@ -340,7 +340,10 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel override dispose() { this._onWillDispose.fire(); this._undoService.removeElements(this.uri); + dispose(this._cellListeners.values()); + this._cellListeners.clear(); + dispose(this._cells); super.dispose(); } diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts index a9838d340f1..8b96a3d29b6 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts @@ -25,7 +25,7 @@ import { bufferToReadable, bufferToStream, streamToBuffer, VSBuffer, VSBufferRea import { assertType } from 'vs/base/common/types'; import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; import { StoredFileWorkingCopyState, IStoredFileWorkingCopy, IStoredFileWorkingCopyModel, IStoredFileWorkingCopyModelContentChangedEvent, IStoredFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopy'; -import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { canceled } from 'vs/base/common/errors'; import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -443,10 +443,10 @@ export class ComplexNotebookEditorModel extends EditorModel implements INotebook export class SimpleNotebookEditorModel extends EditorModel implements INotebookEditorModel { - private readonly _onDidChangeDirty = new Emitter(); - private readonly _onDidSave = new Emitter(); - private readonly _onDidChangeOrphaned = new Emitter(); - private readonly _onDidChangeReadonly = new Emitter(); + private readonly _onDidChangeDirty = this._register(new Emitter()); + private readonly _onDidSave = this._register(new Emitter()); + private readonly _onDidChangeOrphaned = this._register(new Emitter()); + private readonly _onDidChangeReadonly = this._register(new Emitter()); readonly onDidChangeDirty: Event = this._onDidChangeDirty.event; readonly onDidSave: Event = this._onDidSave.event; @@ -454,7 +454,7 @@ export class SimpleNotebookEditorModel extends EditorModel implements INotebookE readonly onDidChangeReadonly: Event = this._onDidChangeReadonly.event; private _workingCopy?: IStoredFileWorkingCopy | IUntitledFileWorkingCopy; - private readonly _workingCopyListeners = new DisposableStore(); + private readonly _workingCopyListeners = this._register(new DisposableStore()); constructor( readonly resource: URI, @@ -468,12 +468,7 @@ export class SimpleNotebookEditorModel extends EditorModel implements INotebookE } override dispose(): void { - this._workingCopyListeners.dispose(); this._workingCopy?.dispose(); - this._onDidChangeDirty.dispose(); - this._onDidSave.dispose(); - this._onDidChangeOrphaned.dispose(); - this._onDidChangeReadonly.dispose(); super.dispose(); } @@ -566,21 +561,22 @@ export class SimpleNotebookEditorModel extends EditorModel implements INotebookE } } -export class NotebookFileWorkingCopyModel implements IStoredFileWorkingCopyModel, IUntitledFileWorkingCopyModel { - - private readonly _onDidChangeContent = new Emitter(); - private readonly _changeListener: IDisposable; +export class NotebookFileWorkingCopyModel extends Disposable implements IStoredFileWorkingCopyModel, IUntitledFileWorkingCopyModel { + private readonly _onDidChangeContent = this._register(new Emitter()); readonly onDidChangeContent = this._onDidChangeContent.event; + readonly onWillDispose: Event; constructor( private readonly _notebookModel: NotebookTextModel, private readonly _notebookSerializer: INotebookSerializer ) { + super(); + this.onWillDispose = _notebookModel.onWillDispose.bind(_notebookModel); - this._changeListener = _notebookModel.onDidChangeContent(e => { + this._register(_notebookModel.onDidChangeContent(e => { for (const rawEvent of e.rawEvents) { if (rawEvent.kind === NotebookCellsChangeType.Initialize) { continue; @@ -595,13 +591,12 @@ export class NotebookFileWorkingCopyModel implements IStoredFileWorkingCopyModel }); break; } - }); + })); } - dispose(): void { - this._changeListener.dispose(); - this._onDidChangeContent.dispose(); + override dispose(): void { this._notebookModel.dispose(); + super.dispose(); } get notebookModel() { diff --git a/src/vs/workbench/contrib/notebook/common/notebookKeymapService.ts b/src/vs/workbench/contrib/notebook/common/notebookKeymapService.ts new file mode 100644 index 00000000000..a06f4e0e495 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/common/notebookKeymapService.ts @@ -0,0 +1,12 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; + +export const INotebookKeymapService = createDecorator('notebookKeymapService'); + +export interface INotebookKeymapService { + readonly _serviceBrand: undefined; +} diff --git a/src/vs/workbench/contrib/notebook/common/notebookOptions.ts b/src/vs/workbench/contrib/notebook/common/notebookOptions.ts index cac1086818d..b0cee081533 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookOptions.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookOptions.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter } from 'vs/base/common/event'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; import { IConfigurationChangeEvent, IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { CellToolbarLocation, CellToolbarVisibility, CompactView, ConsolidatedOutputButton, ConsolidatedRunButton, DragAndDropEnabled, ExperimentalInsertToolbarAlignment, FocusIndicator, GlobalToolbar, InsertToolbarLocation, NotebookCellEditorOptionsCustomizations, NotebookCellInternalMetadata, ShowCellStatusBar, ShowCellStatusBarType, ShowFoldingControls } from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -100,13 +100,13 @@ const compactConfigConstants = { focusIndicatorLeftMargin: 4 }; -export class NotebookOptions { +export class NotebookOptions extends Disposable { private _layoutConfiguration: NotebookLayoutConfiguration; - protected readonly _onDidChangeOptions = new Emitter(); + protected readonly _onDidChangeOptions = this._register(new Emitter()); readonly onDidChangeOptions = this._onDidChangeOptions.event; - private _disposables: IDisposable[]; constructor(private readonly configurationService: IConfigurationService) { + super(); const showCellStatusBar = this.configurationService.getValue(ShowCellStatusBar); const globalToolbar = this.configurationService.getValue(GlobalToolbar) ?? true; const consolidatedOutputButton = this.configurationService.getValue(ConsolidatedOutputButton) ?? true; @@ -123,7 +123,6 @@ export class NotebookOptions { const fontSize = this.configurationService.getValue('editor.fontSize'); const editorOptionsCustomizations = this.configurationService.getValue(NotebookCellEditorOptionsCustomizations); - this._disposables = []; this._layoutConfiguration = { ...(compactView ? compactConfigConstants : defaultConfigConstants), cellTopMargin: 6, @@ -155,11 +154,11 @@ export class NotebookOptions { editorOptionsCustomizations, }; - this._disposables.push(this.configurationService.onDidChangeConfiguration(e => { + this._register(this.configurationService.onDidChangeConfiguration(e => { this._updateConfiguration(e); })); - this._disposables.push(EditorTopPaddingChangeEvent(() => { + this._register(EditorTopPaddingChangeEvent(() => { const configuration = Object.assign({}, this._layoutConfiguration); configuration.editorTopPadding = getEditorTopPadding(); this._layoutConfiguration = configuration; @@ -486,9 +485,4 @@ export class NotebookOptions { this._layoutConfiguration = { ...this._layoutConfiguration, ...{ cellBreakpointMarginActive: active } }; this._onDidChangeOptions.fire({ cellBreakpointMargin: true }); } - - dispose() { - this._disposables.forEach(d => d.dispose()); - this._disposables = []; - } } diff --git a/src/vs/workbench/contrib/notebook/test/cellOutput.test.ts b/src/vs/workbench/contrib/notebook/test/cellOutput.test.ts index 155d8856b67..1cec8d0b373 100644 --- a/src/vs/workbench/contrib/notebook/test/cellOutput.test.ts +++ b/src/vs/workbench/contrib/notebook/test/cellOutput.test.ts @@ -5,6 +5,7 @@ import * as assert from 'assert'; import * as DOM from 'vs/base/browser/dom'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { mock } from 'vs/base/test/common/mock'; import { IMenuService } from 'vs/platform/actions/common/actions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; @@ -95,7 +96,8 @@ suite('NotebookViewModel Outputs', async () => { getContentHeight: () => { return 100; } - } + }, + disposables: new DisposableStore(), } as unknown as CodeCellRenderTemplate, { limit: 5 }, openerService, instantiationService); container.render(100); assert.strictEqual(container.renderedOutputEntries.length, 4); @@ -173,7 +175,8 @@ suite('NotebookViewModel Outputs', async () => { getContentHeight: () => { return 100; } - } + }, + disposables: new DisposableStore(), } as unknown as CodeCellRenderTemplate, { limit: 5 }, openerService, instantiationService); container.render(100); assert.strictEqual(container.renderedOutputEntries.length, 5); diff --git a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts index af66f24f1de..85a3a4d0452 100644 --- a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts +++ b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts @@ -1302,7 +1302,7 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { if (terminal.group === group) { const originalInstance = terminal.terminal; await originalInstance.waitForTitle(); - result = await this.terminalService.createTerminal({ instanceToSplit: originalInstance, config: launchConfigs }); + result = await this.terminalService.createTerminal({ location: { parentTerminal: originalInstance }, config: launchConfigs }); if (result) { break; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index f51867a263e..07436d4c0e8 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -8,7 +8,7 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { FindReplaceState } from 'vs/editor/contrib/find/findState'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensions, ITerminalLaunchError, ITerminalProfile, ITerminalTabLayoutInfoById, TerminalIcon, TitleEventSource, TerminalShellType, ICreateContributedTerminalProfileOptions, TerminalLocation, IExtensionTerminalProfile, ITerminalProfileType } from 'vs/platform/terminal/common/terminal'; +import { IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensions, ITerminalLaunchError, ITerminalProfile, ITerminalTabLayoutInfoById, TerminalIcon, TitleEventSource, TerminalShellType, IExtensionTerminalProfile, ITerminalProfileType, TerminalLocation, ICreateContributedTerminalProfileOptions } from 'vs/platform/terminal/common/terminal'; import { ICommandTracker, INavigationMode, IOffProcessTerminalService, IRemoteTerminalAttachTarget, IStartExtensionTerminalRequest, ITerminalConfigHelper, ITerminalProcessExtHostProxy } from 'vs/workbench/contrib/terminal/common/terminal'; import type { Terminal as XTermTerminal } from 'xterm'; import type { SearchAddon as XTermSearchAddon } from 'xterm-addon-search'; @@ -20,6 +20,7 @@ import { Orientation } from 'vs/base/browser/ui/splitview/splitview'; import { IEditableData } from 'vs/workbench/common/views'; import { DeserializedTerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/terminalEditorSerializer'; import { TerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/terminalEditorInput'; +import { EditorGroupColumn } from 'vs/workbench/services/editor/common/editorGroupColumn'; export const ITerminalService = createDecorator('terminalService'); export const ITerminalEditorService = createDecorator('terminalEditorService'); @@ -110,6 +111,7 @@ export interface ITerminalService extends ITerminalInstanceHost { readonly availableProfiles: ITerminalProfile[]; readonly allProfiles: ITerminalProfileType[] | undefined; readonly profilesReady: Promise; + readonly defaultLocation: TerminalLocation; initializeTerminals(): Promise; onDidChangeActiveGroup: Event; @@ -182,7 +184,7 @@ export interface ITerminalService extends ITerminalInstanceHost { safeDisposeTerminal(instance: ITerminalInstance): Promise; getDefaultInstanceHost(): ITerminalInstanceHost; - getInstanceHost(target: TerminalLocation | undefined): ITerminalInstanceHost; + getInstanceHost(target: ITerminalLocationOptions | undefined): ITerminalInstanceHost; getFindHost(instance?: ITerminalInstance): ITerminalFindHost; getDefaultProfileName(): string; @@ -198,7 +200,7 @@ export interface ITerminalEditorService extends ITerminalInstanceHost, ITerminal /** Gets all _terminal editor_ instances. */ readonly instances: readonly ITerminalInstance[]; - openEditor(instance: ITerminalInstance, sideGroup?: boolean): Promise; + openEditor(instance: ITerminalInstance, editorOptions?: TerminalEditorLocation): Promise; detachActiveEditorInstance(): ITerminalInstance; detachInstance(instance: ITerminalInstance): void; splitInstance(instanceToSplit: ITerminalInstance, shellLaunchConfig?: IShellLaunchConfig): ITerminalInstance; @@ -208,6 +210,8 @@ export interface ITerminalEditorService extends ITerminalInstanceHost, ITerminal getInputFromResource(resource: URI): TerminalEditorInput; } +export type ITerminalLocationOptions = TerminalLocation | TerminalEditorLocation | { parentTerminal: ITerminalInstance } | { splitActiveTerminal: boolean }; + export interface ICreateTerminalOptions { /** * The shell launch config or profile to launch with, when not specified the default terminal @@ -219,23 +223,20 @@ export interface ICreateTerminalOptions { * specified. */ cwd?: string | URI; - /** - * Where to create the terminal, when not specified the default target will be used. - */ - target?: TerminalLocation; - /** - * Creates a split terminal without requiring a terminal instance to split, for example when splitting - * a terminal editor - */ - forceSplit?: boolean; /** * The terminal's resource, passed when the terminal has moved windows. */ resource?: URI; + /** - * The terminal instance to split + * The terminal's location (editor or panel), it's terminal parent (split to the right), or editor group */ - instanceToSplit?: ITerminalInstance; + location?: ITerminalLocationOptions; +} + +export interface TerminalEditorLocation { + viewColumn: EditorGroupColumn, + preserveFocus?: boolean } /** diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index 5b1356ad766..0b4372a8765 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -44,6 +44,7 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/ import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; export const switchTerminalActionViewItemSeparator = '─────────'; export const switchTerminalShowTabsTitle = localize('showTerminalTabs', "Show Tabs"); @@ -112,7 +113,7 @@ export function registerTerminalActions() { constructor() { super({ id: TerminalCommandId.NewInActiveWorkspace, - title: { value: localize('workbench.action.terminal.newInActiveWorkspace', "Create New Integrated Terminal (In Active Workspace)"), original: 'Create New Integrated Terminal (In Active Workspace)' }, + title: { value: localize('workbench.action.terminal.newInActiveWorkspace', "Create New Terminal (In Active Workspace)"), original: 'Create New Terminal (In Active Workspace)' }, f1: true, category, precondition: TerminalContextKeys.processSupported @@ -122,7 +123,7 @@ export function registerTerminalActions() { const terminalService = accessor.get(ITerminalService); const terminalGroupService = accessor.get(ITerminalGroupService); if (terminalService.isProcessSupportRegistered) { - const instance = await terminalService.createTerminal({ target: terminalService.configHelper.config.defaultLocation }); + const instance = await terminalService.createTerminal({ location: terminalService.defaultLocation }); if (!instance) { return; } @@ -139,7 +140,7 @@ export function registerTerminalActions() { constructor() { super({ id: TerminalCommandId.CreateTerminalEditor, - title: { value: localize('workbench.action.terminal.createTerminalEditor', "Create Terminal in Editor Area"), original: 'Create Terminal in Editor Area' }, + title: { value: localize('workbench.action.terminal.createTerminalEditor', "Create New Terminal in Editor Area"), original: 'Create New Terminal in Editor Area' }, f1: true, category, precondition: TerminalContextKeys.processSupported @@ -148,7 +149,7 @@ export function registerTerminalActions() { async run(accessor: ServicesAccessor) { const terminalService = accessor.get(ITerminalService); const instance = await terminalService.createTerminal({ - target: TerminalLocation.Editor + location: TerminalLocation.Editor }); instance.focusWhenReady(); } @@ -158,7 +159,7 @@ export function registerTerminalActions() { constructor() { super({ id: TerminalCommandId.CreateTerminalEditorSide, - title: { value: localize('workbench.action.terminal.createTerminalEditorSide', "Create Terminal in Editor Area to the Side"), original: 'Create Terminal in Editor Area to the Side' }, + title: { value: localize('workbench.action.terminal.createTerminalEditorSide', "Create New Terminal in Editor Area to the Side"), original: 'Create New Terminal in Editor Area to the Side' }, f1: true, category, precondition: TerminalContextKeys.processSupported @@ -167,8 +168,7 @@ export function registerTerminalActions() { async run(accessor: ServicesAccessor) { const terminalService = accessor.get(ITerminalService); const instance = await terminalService.createTerminal({ - target: TerminalLocation.Editor, - forceSplit: true + location: { viewColumn: SIDE_GROUP } }); instance.focusWhenReady(); } @@ -386,7 +386,7 @@ export function registerTerminalActions() { async run(accessor: ServicesAccessor) { const terminalService = accessor.get(ITerminalService); const terminalGroupService = accessor.get(ITerminalGroupService); - const instance = terminalService.activeInstance || await terminalService.createTerminal({ target: TerminalLocation.TerminalView }); + const instance = terminalService.activeInstance || await terminalService.createTerminal({ location: TerminalLocation.Panel }); if (!instance) { return; } @@ -1137,10 +1137,10 @@ export function registerTerminalActions() { }); registerAction2(class extends Action2 { constructor() { - const title = localize('workbench.action.terminal.newWithCwd', "Create New Integrated Terminal Starting in a Custom Working Directory"); + const title = localize('workbench.action.terminal.newWithCwd', "Create New Terminal Starting in a Custom Working Directory"); super({ id: TerminalCommandId.NewWithCwd, - title: { value: title, original: 'Create New Integrated Terminal Starting in a Custom Working Directory' }, + title: { value: title, original: 'Create New Terminal Starting in a Custom Working Directory' }, category, description: { description: title, @@ -1425,7 +1425,7 @@ export function registerTerminalActions() { const terminalService = accessor.get(ITerminalService); const workspaceContextService = accessor.get(IWorkspaceContextService); const options = convertOptionsOrProfileToOptions(optionsOrProfile); - const activeInstance = terminalService.getInstanceHost(options?.target).activeInstance; + const activeInstance = terminalService.getInstanceHost(options?.location).activeInstance; if (!activeInstance) { return; } @@ -1433,7 +1433,7 @@ export function registerTerminalActions() { if (cwd === undefined) { return undefined; } - const instance = await terminalService.createTerminal({ instanceToSplit: activeInstance, config: options?.config, cwd, target: activeInstance.target }); + const instance = await terminalService.createTerminal({ location: { parentTerminal: activeInstance }, config: options?.config, cwd }); if (instance) { if (instance.target === TerminalLocation.Editor) { instance.focusWhenReady(); @@ -1471,7 +1471,7 @@ export function registerTerminalActions() { terminalService.setActiveInstance(t); terminalService.doWithActiveInstance(async instance => { const cwd = await getCwdForSplit(terminalService.configHelper, instance); - await terminalService.createTerminal({ instanceToSplit: instance, cwd }); + await terminalService.createTerminal({ location: { parentTerminal: instance }, cwd }); await terminalGroupService.showPanel(true); }); } @@ -1546,7 +1546,7 @@ export function registerTerminalActions() { const terminalGroupService = accessor.get(ITerminalGroupService); await terminalService.doWithActiveInstance(async t => { const cwd = await getCwdForSplit(terminalService.configHelper, t); - const instance = await terminalService.createTerminal({ instanceToSplit: t, cwd }); + const instance = await terminalService.createTerminal({ location: { parentTerminal: t }, cwd }); if (instance?.target !== TerminalLocation.Editor) { await terminalGroupService.showPanel(true); } @@ -1582,7 +1582,7 @@ export function registerTerminalActions() { constructor() { super({ id: TerminalCommandId.New, - title: { value: localize('workbench.action.terminal.new', "Create New Integrated Terminal"), original: 'Create New Integrated Terminal' }, + title: { value: localize('workbench.action.terminal.new', "Create New Terminal"), original: 'Create New Terminal' }, f1: true, category, precondition: TerminalContextKeys.processSupported, @@ -1613,14 +1613,14 @@ export function registerTerminalActions() { const activeInstance = terminalService.activeInstance; if (activeInstance) { const cwd = await getCwdForSplit(terminalService.configHelper, activeInstance); - await terminalService.createTerminal({ instanceToSplit: activeInstance, cwd }); + await terminalService.createTerminal({ location: { parentTerminal: activeInstance }, cwd }); return; } } if (terminalService.isProcessSupportRegistered) { eventOrOptions = !eventOrOptions || eventOrOptions instanceof MouseEvent ? {} : eventOrOptions; - eventOrOptions.target = eventOrOptions.target || terminalService.configHelper.config.defaultLocation; + eventOrOptions.location = eventOrOptions.location || terminalService.defaultLocation; let instance: ITerminalInstance | undefined; if (folders.length <= 1) { // Allow terminal service to handle the path when there is only a @@ -1979,7 +1979,7 @@ export function refreshTerminalActions(detectedProfiles: ITerminalProfile[]) { constructor() { super({ id: TerminalCommandId.NewWithProfile, - title: { value: localize('workbench.action.terminal.newWithProfile', "Create New Integrated Terminal (With Profile)"), original: 'Create New Integrated Terminal (With Profile)' }, + title: { value: localize('workbench.action.terminal.newWithProfile', "Create New Terminal (With Profile)"), original: 'Create New Terminal (With Profile)' }, f1: true, category, precondition: TerminalContextKeys.processSupported, @@ -2026,10 +2026,10 @@ export function refreshTerminalActions(detectedProfiles: ITerminalProfile[]) { const folders = workspaceContextService.getWorkspace().folders; if (event && (event.altKey || event.ctrlKey)) { - const activeInstance = terminalService.activeInstance; - if (activeInstance) { - const cwd = await getCwdForSplit(terminalService.configHelper, activeInstance); - await terminalService.createTerminal({ instanceToSplit: activeInstance, config: options?.config, cwd }); + const parentTerminal = terminalService.activeInstance; + if (parentTerminal) { + const cwd = await getCwdForSplit(terminalService.configHelper, parentTerminal); + await terminalService.createTerminal({ location: { parentTerminal }, config: options?.config, cwd }); return; } } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts b/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts index 3b96d099d46..5378eddcc56 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts @@ -16,7 +16,6 @@ import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { TerminalLocation } from 'vs/platform/terminal/common/terminal'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { IEditorOpenContext } from 'vs/workbench/common/editor'; @@ -32,6 +31,7 @@ import { BrowserFeatures } from 'vs/base/browser/canIUse'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { openContextMenu } from 'vs/workbench/contrib/terminal/browser/terminalContextMenu'; import { ICommandService } from 'vs/platform/commands/common/commands'; +import { TerminalLocation } from 'vs/platform/terminal/common/terminal'; const findWidgetSelector = '.simple-find-part-wrapper'; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalEditorService.ts b/src/vs/workbench/contrib/terminal/browser/terminalEditorService.ts index 1cd21d81fc3..a13d8ffcb90 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalEditorService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalEditorService.ts @@ -11,7 +11,7 @@ import { EditorActivation } from 'vs/platform/editor/common/editor'; import { IInstantiationService, optional } from 'vs/platform/instantiation/common/instantiation'; import { IShellLaunchConfig, TerminalLocation } from 'vs/platform/terminal/common/terminal'; import { IEditorInput } from 'vs/workbench/common/editor'; -import { IRemoteTerminalService, ITerminalEditorService, ITerminalInstance, ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { IRemoteTerminalService, ITerminalEditorService, ITerminalInstance, ITerminalInstanceService, TerminalEditorLocation } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalEditor } from 'vs/workbench/contrib/terminal/browser/terminalEditor'; import { TerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/terminalEditorInput'; import { DeserializedTerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/terminalEditorSerializer'; @@ -158,7 +158,7 @@ export class TerminalEditorService extends Disposable implements ITerminalEditor this._onDidChangeActiveInstance.fire(this.activeInstance); } - async openEditor(instance: ITerminalInstance, sideGroup: boolean = false): Promise { + async openEditor(instance: ITerminalInstance, editorOptions?: TerminalEditorLocation): Promise { const resource = this.resolveResource(instance); if (resource) { await this._editorService.openEditor({ @@ -166,10 +166,10 @@ export class TerminalEditorService extends Disposable implements ITerminalEditor options: { pinned: true, - forceReload: true + forceReload: true, + preserveFocus: editorOptions?.preserveFocus } - }, - sideGroup ? SIDE_GROUP : undefined); + }, editorOptions?.viewColumn); } } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index f0bee88a4a6..d5254e61a67 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -1511,7 +1511,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._safeSetOption('macOptionClickForcesSelection', config.macOptionClickForcesSelection); this._safeSetOption('rightClickSelectsWord', config.rightClickBehavior === 'selectWord'); this._safeSetOption('wordSeparator', config.wordSeparators); - + this._safeSetOption('customGlyphs', config.customGlyphs); const suggestedRendererType = TerminalInstance._suggestedRendererType; // @meganrogge @Tyriar remove if the issue related to iPads and webgl is resolved if ((!isIOS && config.gpuAcceleration === 'auto' && suggestedRendererType === undefined) || config.gpuAcceleration === 'on') { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts b/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts index 0468c8df956..d80f3b6e1a8 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts @@ -17,6 +17,7 @@ import { ICreateTerminalOptions, ITerminalService } from 'vs/workbench/contrib/t import { TerminalCommandId, TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal'; import { TerminalContextKeys, TerminalContextKeyStrings } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; import { terminalStrings } from 'vs/workbench/contrib/terminal/common/terminalStrings'; +import { SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; const enum ContextMenuGroup { Create = '1_create', @@ -571,7 +572,7 @@ export function setupTerminalMenus(): void { }); } -export function getTerminalActionBarArgs(target: TerminalLocation, profiles: ITerminalProfile[], defaultProfileName: string, contributedProfiles: readonly IExtensionTerminalProfile[], instantiationService: IInstantiationService, terminalService: ITerminalService, contextKeyService: IContextKeyService, commandService: ICommandService, dropdownMenu: IMenu): { +export function getTerminalActionBarArgs(location: TerminalLocation, profiles: ITerminalProfile[], defaultProfileName: string, contributedProfiles: readonly IExtensionTerminalProfile[], instantiationService: IInstantiationService, terminalService: ITerminalService, contextKeyService: IContextKeyService, commandService: ICommandService, dropdownMenu: IMenu): { primaryAction: MenuItemAction, dropdownAction: IAction, dropdownMenuActions: IAction[], @@ -586,7 +587,7 @@ export function getTerminalActionBarArgs(target: TerminalLocation, profiles: ITe const options: IMenuActionOptions = { arg: { config: p, - target + location } as ICreateTerminalOptions, shouldForwardArgs: true }; @@ -608,17 +609,16 @@ export function getTerminalActionBarArgs(target: TerminalLocation, profiles: ITe id: contributed.id, title }, - forceSplit: false, - target + location }))); + const splitLocation = location === TerminalLocation.Editor ? { viewColumn: SIDE_GROUP } : location; submenuActions.push(new Action(TerminalCommandId.NewWithProfile, title, undefined, true, () => terminalService.createTerminal({ config: { extensionIdentifier: contributed.extensionIdentifier, id: contributed.id, title }, - forceSplit: true, - target + location: splitLocation }))); } @@ -651,7 +651,7 @@ export function getTerminalActionBarArgs(target: TerminalLocation, profiles: ITe const primaryAction = instantiationService.createInstance( MenuItemAction, { - id: target === TerminalLocation.TerminalView ? TerminalCommandId.New : TerminalCommandId.CreateTerminalEditor, + id: location === TerminalLocation.Panel ? TerminalCommandId.New : TerminalCommandId.CreateTerminalEditor, title: localize('terminal.new', "New Terminal"), icon: Codicon.plus }, @@ -662,7 +662,7 @@ export function getTerminalActionBarArgs(target: TerminalLocation, profiles: ITe }, { shouldForwardArgs: true, - arg: { target } as ICreateTerminalOptions, + arg: { location } as ICreateTerminalOptions, }); const dropdownAction = new Action('refresh profiles', 'Launch Profile...', 'codicon-chevron-down', true); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index 09a6d9ddeb1..a3937089afd 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -23,7 +23,7 @@ import { ILabelService } from 'vs/platform/label/common/label'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IKeyMods, IPickOptions, IQuickInputButton, IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { ICreateContributedTerminalProfileOptions, IExtensionTerminalProfile, IShellLaunchConfig, ITerminalLaunchError, ITerminalProfile, ITerminalProfileObject, ITerminalProfileType, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, TerminalLocation, TerminalSettingId, TerminalSettingPrefix } from 'vs/platform/terminal/common/terminal'; +import { ICreateContributedTerminalProfileOptions, IExtensionTerminalProfile, IShellLaunchConfig, ITerminalLaunchError, ITerminalProfile, ITerminalProfileObject, ITerminalProfileType, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, TerminalLocation, TerminalLocationString, TerminalSettingId, TerminalSettingPrefix } from 'vs/platform/terminal/common/terminal'; import { registerTerminalDefaultProfileConfiguration } from 'vs/platform/terminal/common/terminalPlatformConfiguration'; import { iconForeground } from 'vs/platform/theme/common/colorRegistry'; import { IconDefinition } from 'vs/platform/theme/common/iconRegistry'; @@ -31,7 +31,7 @@ import { ColorScheme } from 'vs/platform/theme/common/theme'; import { IThemeService, Themable, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { VirtualWorkspaceContext } from 'vs/workbench/browser/contextkeys'; import { IEditableData, IViewsService } from 'vs/workbench/common/views'; -import { ICreateTerminalOptions, IRemoteTerminalService, IRequestAddInstanceToGroupEvent, ITerminalEditorService, ITerminalExternalLinkProvider, ITerminalFindHost, ITerminalGroup, ITerminalGroupService, ITerminalInstance, ITerminalInstanceHost, ITerminalInstanceService, ITerminalProfileProvider, ITerminalService, TerminalConnectionState } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ICreateTerminalOptions, IRemoteTerminalService, IRequestAddInstanceToGroupEvent, ITerminalEditorService, ITerminalExternalLinkProvider, ITerminalFindHost, ITerminalGroup, ITerminalGroupService, ITerminalInstance, ITerminalInstanceHost, ITerminalInstanceService, ITerminalLocationOptions, ITerminalProfileProvider, ITerminalService, TerminalConnectionState, TerminalEditorLocation } from 'vs/workbench/contrib/terminal/browser/terminal'; import { refreshTerminalActions } from 'vs/workbench/contrib/terminal/browser/terminalActions'; import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper'; import { TerminalEditor } from 'vs/workbench/contrib/terminal/browser/terminalEditor'; @@ -96,6 +96,8 @@ export class TerminalService implements ITerminalService { return this._terminalGroupService.instances.concat(this._terminalEditorService.instances); } + get defaultLocation(): TerminalLocation { return this.configHelper.config.defaultLocation === TerminalLocationString.Editor ? TerminalLocation.Editor : TerminalLocation.Panel; } + private _activeInstance: ITerminalInstance | undefined; get activeInstance(): ITerminalInstance | undefined { // Check if either an editor or panel terminal has focus and return that, regardless of the @@ -412,7 +414,7 @@ export class TerminalService implements ITerminalService { // create group and terminal terminalInstance = await this.createTerminal({ config: { attachPersistentProcess: terminalLayout.terminal! }, - target: TerminalLocation.TerminalView + location: TerminalLocation.Panel }); group = this._terminalGroupService.getGroupForInstance(terminalInstance); if (groupLayout.isActive) { @@ -420,7 +422,7 @@ export class TerminalService implements ITerminalService { } } else { // add split terminals to this group - await this.createTerminal({ config: { attachPersistentProcess: terminalLayout.terminal! }, instanceToSplit: terminalInstance },); + await this.createTerminal({ config: { attachPersistentProcess: terminalLayout.terminal! }, location: { parentTerminal: terminalInstance } }); } } const activeInstance = this.instances.find(t => { @@ -646,7 +648,7 @@ export class TerminalService implements ITerminalService { await this._localTerminalsInitPromise; } if (this._terminalGroupService.groups.length === 0 && this.isProcessSupportRegistered) { - this.createTerminal({ target: TerminalLocation.TerminalView }); + this.createTerminal({ location: TerminalLocation.Panel }); } } @@ -679,7 +681,7 @@ export class TerminalService implements ITerminalService { if (source.target !== TerminalLocation.Editor) { return; } - source.target = TerminalLocation.TerminalView; + source.target = TerminalLocation.Panel; let group: ITerminalGroup | undefined; if (target) { @@ -937,7 +939,7 @@ export class TerminalService implements ITerminalService { if ('id' in value.profile) { await this._createContributedTerminalProfile(value.profile.extensionIdentifier, value.profile.id, { - isSplitTerminal: !!(keyMods?.alt && activeInstance), + splitActiveTerminal: !!(keyMods?.alt && activeInstance), icon: value.profile.icon, color: value.profile.color }); @@ -945,13 +947,13 @@ export class TerminalService implements ITerminalService { } else { if (keyMods?.alt && activeInstance) { // create split, only valid if there's an active instance - instance = await this.createTerminal({ instanceToSplit: activeInstance, config: value.profile }); + instance = await this.createTerminal({ location: { parentTerminal: activeInstance }, config: value.profile }); } else { - instance = await this.createTerminal({ target: this.configHelper.config.defaultLocation, config: value.profile, cwd }); + instance = await this.createTerminal({ location: this.defaultLocation, config: value.profile, cwd }); } } - if (instance && this.configHelper.config.defaultLocation !== TerminalLocation.Editor) { + if (instance && this.defaultLocation !== TerminalLocation.Editor) { this._terminalGroupService.showPanel(true); this.setActiveInstance(instance); return instance; @@ -992,18 +994,25 @@ export class TerminalService implements ITerminalService { getDefaultInstanceHost(): ITerminalInstanceHost { - if (this.configHelper.config.defaultLocation === TerminalLocation.Editor) { + if (this.defaultLocation === TerminalLocation.Editor) { return this._terminalEditorService; } return this._terminalGroupService; } - getInstanceHost(target: TerminalLocation | undefined): ITerminalInstanceHost { - if (target) { - if (target === TerminalLocation.Editor) { + getInstanceHost(location: ITerminalLocationOptions | undefined): ITerminalInstanceHost { + if (location) { + if (location === TerminalLocation.Editor) { return this._terminalEditorService; + } else if (typeof location === 'object') { + if ('viewColumn' in location) { + return this._terminalEditorService; + } else if ('parentTerminal' in location) { + return location.parentTerminal.target === TerminalLocation.Editor ? this._terminalEditorService : this._terminalGroupService; + } + } else { + return this._terminalGroupService; } - return this._terminalGroupService; } return this; } @@ -1126,12 +1135,12 @@ export class TerminalService implements ITerminalService { // Launch the contributed profile if (contributedProfile) { await this._createContributedTerminalProfile(contributedProfile.extensionIdentifier, contributedProfile.id, { - isSplitTerminal: options?.forceSplit || !!options?.instanceToSplit, icon: contributedProfile.icon, - target: options?.target, - color: contributedProfile.color + color: contributedProfile.color, + splitActiveTerminal: typeof options?.location === 'object' && 'splitActiveTerminal' in options.location ? true : false }); - const instanceHost = options?.target === TerminalLocation.Editor ? this._terminalEditorService : this._terminalGroupService; + // TODO shouldn't the below use defaultLocation? + const instanceHost = options?.location === TerminalLocation.Editor ? this._terminalEditorService : this._terminalGroupService; const instance = instanceHost.instances[instanceHost.instances.length - 1]; await instance.focusWhenReady(); return instance; @@ -1141,15 +1150,6 @@ export class TerminalService implements ITerminalService { shellLaunchConfig.cwd = options.cwd; } - // Use the URI from the base instance if it exists, this will correctly split local terminals - if (options?.instanceToSplit && typeof shellLaunchConfig.cwd !== 'object' && typeof options.instanceToSplit.shellLaunchConfig.cwd === 'object') { - shellLaunchConfig.cwd = URI.from({ - scheme: options.instanceToSplit.shellLaunchConfig.cwd.scheme, - authority: options.instanceToSplit.shellLaunchConfig.cwd.authority, - path: shellLaunchConfig.cwd || options.instanceToSplit.shellLaunchConfig.cwd.path - }); - } - if (!shellLaunchConfig.customPtyImplementation && !this.isProcessSupportRegistered) { throw new Error('Could not create terminal when process support is not registered'); } @@ -1163,34 +1163,85 @@ export class TerminalService implements ITerminalService { } this._evaluateLocalCwd(shellLaunchConfig); + const location = this._resolveLocation(options?.location) || this.defaultLocation; + const parent = this._getSplitParent(options?.location); - let instance: ITerminalInstance; - const target = options?.target || this.configHelper.config.defaultLocation; - if (options?.instanceToSplit) { - if (target === TerminalLocation.Editor || options?.instanceToSplit?.target === TerminalLocation.Editor) { - instance = this._terminalEditorService.splitInstance(options.instanceToSplit, shellLaunchConfig); - } else { - const group = this._terminalGroupService.getGroupForInstance(options.instanceToSplit); - if (!group) { - throw new Error(`Cannot split a terminal without a group ${options.instanceToSplit}`); - } - instance = group.split(shellLaunchConfig); - this._terminalGroupService.groups.forEach((g, i) => g.setVisible(i === this._terminalGroupService.activeGroupIndex)); - } + if (parent) { + return this._splitTerminal(shellLaunchConfig, location, parent); } else { - if (target === TerminalLocation.Editor) { - instance = this._terminalInstanceService.createInstance(shellLaunchConfig, undefined, options?.resource); - instance.target = TerminalLocation.Editor; - this._terminalEditorService.openEditor(instance, options?.forceSplit || !!options?.instanceToSplit); - } else { - // TODO: pass resource? - const group = this._terminalGroupService.createGroup(shellLaunchConfig); - instance = group.terminalInstances[0]; + return this._createTerminal(shellLaunchConfig, location, options); + } + } + + private _splitTerminal(shellLaunchConfig: IShellLaunchConfig, location: TerminalLocation, parent: ITerminalInstance): ITerminalInstance { + let instance; + // Use the URI from the base instance if it exists, this will correctly split local terminals + if (typeof shellLaunchConfig.cwd !== 'object' && typeof parent.shellLaunchConfig.cwd === 'object') { + shellLaunchConfig.cwd = URI.from({ + scheme: parent.shellLaunchConfig.cwd.scheme, + authority: parent.shellLaunchConfig.cwd.authority, + path: shellLaunchConfig.cwd || parent.shellLaunchConfig.cwd.path + }); + } + if (location === TerminalLocation.Editor || parent.target === TerminalLocation.Editor) { + instance = this._terminalEditorService.splitInstance(parent, shellLaunchConfig); + } else { + const group = this._terminalGroupService.getGroupForInstance(parent); + if (!group) { + throw new Error(`Cannot split a terminal without a group ${parent}`); } + instance = group.split(shellLaunchConfig); + this._terminalGroupService.groups.forEach((g, i) => g.setVisible(i === this._terminalGroupService.activeGroupIndex)); } return instance; } + private _createTerminal(shellLaunchConfig: IShellLaunchConfig, location: TerminalLocation, options?: ICreateTerminalOptions): ITerminalInstance { + let instance; + const editorOptions = this._getEditorOptions(options?.location); + if (location === TerminalLocation.Editor) { + instance = this._terminalInstanceService.createInstance(shellLaunchConfig, undefined, options?.resource); + instance.target = TerminalLocation.Editor; + this._terminalEditorService.openEditor(instance, editorOptions); + } else { + // TODO: pass resource? + const group = this._terminalGroupService.createGroup(shellLaunchConfig); + instance = group.terminalInstances[0]; + } + return instance; + } + + private _resolveLocation(location?: ITerminalLocationOptions): TerminalLocation | undefined { + if (!location) { + return location; + } else if (typeof location === 'object') { + if ('parentTerminal' in location) { + return location.parentTerminal.target; + } else if ('viewColumn' in location) { + return TerminalLocation.Editor; + } else if ('splitActiveTerminal' in location) { + return this._activeInstance?.target || this.defaultLocation; + } + } + return location; + } + + private _getSplitParent(location?: ITerminalLocationOptions): ITerminalInstance | undefined { + if (location && typeof location === 'object' && 'parentTerminal' in location) { + return location.parentTerminal; + } else if (location && typeof location === 'object' && 'splitActiveTerminal' in location) { + return this.activeInstance; + } + return undefined; + } + + private _getEditorOptions(location?: ITerminalLocationOptions): TerminalEditorLocation | undefined { + if (location && typeof location === 'object' && 'viewColumn' in location) { + return location; + } + return undefined; + } + private _evaluateLocalCwd(shellLaunchConfig: IShellLaunchConfig) { // Add welcome message and title annotation for local terminals launched within remote or // virtual workspaces diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts index ee3fc4f730c..7547311d2ee 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts @@ -131,7 +131,7 @@ export class TerminalTabList extends WorkbenchList { this.onMouseDblClick(async e => { const focus = this.getFocus(); if (focus.length === 0) { - const instance = await this._terminalService.createTerminal({ target: TerminalLocation.TerminalView }); + const instance = await this._terminalService.createTerminal({ location: TerminalLocation.Panel }); this._terminalGroupService.setActiveInstance(instance); await instance.focusWhenReady(); } @@ -144,7 +144,7 @@ export class TerminalTabList extends WorkbenchList { // unless multi-selection is in progress this.onMouseClick(async e => { if (e.browserEvent.altKey && e.element) { - await this._terminalService.createTerminal({ instanceToSplit: e.element }); + await this._terminalService.createTerminal({ location: { parentTerminal: e.element } }); } else if (this._getFocusMode() === 'singleClick') { if (this.getSelection().length <= 1) { e.element?.focus(true); @@ -459,7 +459,7 @@ class TerminalTabsRenderer implements IListRenderer { - this._runForSelectionOrInstance(instance, e => this._terminalService.createTerminal({ instanceToSplit: e })); + this._runForSelectionOrInstance(instance, e => this._terminalService.createTerminal({ location: { parentTerminal: e } })); }), new Action(TerminalCommandId.KillInstance, terminalStrings.kill.short, ThemeIcon.asClassName(Codicon.trashcan), true, async () => { this._runForSelectionOrInstance(instance, e => e.dispose()); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalView.ts b/src/vs/workbench/contrib/terminal/browser/terminalView.ts index e6d7737c1bc..6d3d14d466b 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalView.ts @@ -136,7 +136,7 @@ export class TerminalViewPane extends ViewPane { if (this._terminalService.isProcessSupportRegistered) { if (this._terminalsInitialized) { if (!hadTerminals) { - this._terminalService.createTerminal({ target: TerminalLocation.TerminalView }); + this._terminalService.createTerminal({ location: TerminalLocation.Panel }); } } else { this._terminalsInitialized = true; @@ -183,7 +183,7 @@ export class TerminalViewPane extends ViewPane { run: async () => { const instance = this._terminalGroupService.activeInstance; if (instance) { - const newInstance = await this._terminalService.createTerminal({ instanceToSplit: instance, forceSplit: true }); + const newInstance = await this._terminalService.createTerminal({ location: { parentTerminal: instance } }); return newInstance?.focusWhenReady(); } return; @@ -204,8 +204,7 @@ export class TerminalViewPane extends ViewPane { this._tabButtons.dispose(); } - const actions = getTerminalActionBarArgs(TerminalLocation.TerminalView, this._terminalService.availableProfiles, this._getDefaultProfileName(), this._terminalContributionService.terminalProfiles, this._instantiationService, this._terminalService, this._contextKeyService, this._commandService, this._dropdownMenu); - + const actions = getTerminalActionBarArgs(TerminalLocation.Panel, this._terminalService.availableProfiles, this._getDefaultProfileName(), this._terminalContributionService.terminalProfiles, this._instantiationService, this._terminalService, this._contextKeyService, this._commandService, this._dropdownMenu); this._tabButtons = new DropdownWithPrimaryActionViewItem(actions.primaryAction, actions.dropdownAction, actions.dropdownMenuActions, actions.className, this._contextMenuService, {}, this._keybindingService, this._notificationService, this._contextKeyService); this._updateTabActionBar(this._terminalService.availableProfiles); return this._tabButtons; @@ -229,7 +228,7 @@ export class TerminalViewPane extends ViewPane { } private _updateTabActionBar(profiles: ITerminalProfile[]): void { - const actions = getTerminalActionBarArgs(TerminalLocation.TerminalView, profiles, this._getDefaultProfileName(), this._terminalContributionService.terminalProfiles, this._instantiationService, this._terminalService, this._contextKeyService, this._commandService, this._dropdownMenu); + const actions = getTerminalActionBarArgs(TerminalLocation.Panel, profiles, this._getDefaultProfileName(), this._terminalContributionService.terminalProfiles, this._instantiationService, this._terminalService, this._contextKeyService, this._commandService, this._dropdownMenu); this._tabButtons?.update(actions.dropdownAction, actions.dropdownMenuActions); } @@ -388,7 +387,7 @@ class SingleTerminalTabActionViewItem extends MenuEntryActionViewItem { override async onClick(event: MouseEvent): Promise { if (event.altKey && this._menuItemAction.alt) { - this._commandService.executeCommand(this._menuItemAction.alt.id, { target: TerminalLocation.TerminalView } as ICreateTerminalOptions); + this._commandService.executeCommand(this._menuItemAction.alt.id, { target: TerminalLocation.Panel } as ICreateTerminalOptions); } else { this._openContextMenu(); } diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index 0cb577cf6ef..527695acad0 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -8,7 +8,7 @@ import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import { IProcessEnvironment, OperatingSystem } from 'vs/base/common/platform'; import { IExtensionPointDescriptor } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -import { IProcessDataEvent, IProcessReadyEvent, IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensions, ITerminalDimensionsOverride, ITerminalLaunchError, ITerminalProfile, ITerminalProfileObject, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, TerminalIcon, TerminalLocation, TerminalShellType, TitleEventSource } from 'vs/platform/terminal/common/terminal'; +import { IProcessDataEvent, IProcessReadyEvent, IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensions, ITerminalDimensionsOverride, ITerminalLaunchError, ITerminalProfile, ITerminalProfileObject, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, TerminalIcon, TerminalLocationString, TerminalShellType, TitleEventSource } from 'vs/platform/terminal/common/terminal'; import { IEnvironmentVariableInfo } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { URI } from 'vs/base/common/uri'; @@ -203,7 +203,8 @@ export interface ITerminalConfiguration { focusMode: 'singleClick' | 'doubleClick'; }, bellDuration: number; - defaultLocation: TerminalLocation; + defaultLocation: TerminalLocationString; + customGlyphs: boolean; } export const DEFAULT_LOCAL_ECHO_EXCLUDE: ReadonlyArray = ['vim', 'vi', 'nano', 'tmux']; diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index 6d6c24c9c30..2f5a6aea189 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -6,7 +6,7 @@ import { Extensions, IConfigurationNode, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { localize } from 'vs/nls'; import { DEFAULT_LETTER_SPACING, DEFAULT_LINE_HEIGHT, TerminalCursorStyle, DEFAULT_COMMANDS_TO_SKIP_SHELL, SUGGESTIONS_FONT_WEIGHT, MINIMUM_FONT_WEIGHT, MAXIMUM_FONT_WEIGHT, DEFAULT_LOCAL_ECHO_EXCLUDE } from 'vs/workbench/contrib/terminal/common/terminal'; -import { TerminalLocation, TerminalSettingId } from 'vs/platform/terminal/common/terminal'; +import { TerminalLocationString, TerminalSettingId } from 'vs/platform/terminal/common/terminal'; import { isMacintosh, isWindows } from 'vs/base/common/platform'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -78,7 +78,7 @@ const terminalConfiguration: IConfigurationNode = { }, [TerminalSettingId.DefaultLocation]: { type: 'string', - enum: [TerminalLocation.Editor, TerminalLocation.TerminalView], + enum: [TerminalLocationString.Editor, TerminalLocationString.TerminalView], enumDescriptions: [ localize('terminal.integrated.defaultLocation.editor', "Create terminals in the editor"), localize('terminal.integrated.defaultLocation.view', "Create terminals in the terminal view") @@ -451,6 +451,11 @@ const terminalConfiguration: IConfigurationNode = { type: 'boolean', default: true }, + [TerminalSettingId.CustomGlyphs]: { + description: localize('terminal.integrated.customGlyphs', "Whether to draw custom glyphs for block element and box drawing characters instead of using the font, which typically yields better rendering with continuous lines. Note that this doesn't work with the DOM renderer"), + type: 'boolean', + default: true + } } }; diff --git a/src/vs/workbench/contrib/testing/browser/media/testing.css b/src/vs/workbench/contrib/testing/browser/media/testing.css index b4adcdf83f2..8661fe22f47 100644 --- a/src/vs/workbench/contrib/testing/browser/media/testing.css +++ b/src/vs/workbench/contrib/testing/browser/media/testing.css @@ -164,6 +164,10 @@ max-width: none; } +.testing-filter-wrapper { + height: 27px; +} + .testing-filter-action-item .testing-filter-wrapper { flex-grow: 1; } diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts index 85d94133da6..8bab216090a 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts @@ -8,75 +8,176 @@ import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { BaseActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview'; import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem'; -import { HistoryInputBox } from 'vs/base/browser/ui/inputbox/inputBox'; import { Action, IAction, IActionRunner, Separator } from 'vs/base/common/actions'; import { Delayer } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; -import { KeyCode } from 'vs/base/common/keyCodes'; +import { splitGlobAware } from 'vs/base/common/glob'; +import { Iterable } from 'vs/base/common/iterator'; import { localize } from 'vs/nls'; -import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; -import { ContextScopedHistoryInputBox } from 'vs/platform/browser/contextScopedHistoryWidget'; -import { ContextKeyEqualsExpr, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { attachInputBoxStyler } from 'vs/platform/theme/common/styler'; +import { StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { ViewContainerLocation } from 'vs/workbench/common/views'; +import { TestTag } from 'vs/workbench/api/common/extHostTypeConverters'; +import { attachSuggestEnabledInputBoxStyler, ContextScopedSuggestEnabledInputWithHistory, SuggestEnabledInputWithHistory, SuggestResultsProvider } from 'vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput'; import { testingFilterIcon } from 'vs/workbench/contrib/testing/browser/icons'; -import { TestExplorerStateFilter, Testing } from 'vs/workbench/contrib/testing/common/constants'; -import { MutableObservableValue } from 'vs/workbench/contrib/testing/common/observableValue'; +import { Testing } from 'vs/workbench/contrib/testing/common/constants'; +import { IObservableValue, MutableObservableValue } from 'vs/workbench/contrib/testing/common/observableValue'; import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue'; -import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; export interface ITestExplorerFilterState { _serviceBrand: undefined; - readonly text: MutableObservableValue; + readonly text: IObservableValue; + /** Test ID the user wants to reveal in the explorer */ readonly reveal: MutableObservableValue; - readonly stateFilter: MutableObservableValue; - readonly currentDocumentOnly: MutableObservableValue; - /** Whether excluded test should be shown in the view */ - readonly showExcludedTests: MutableObservableValue; readonly onDidRequestInputFocus: Event; + + /** + * Glob list to filter for based on the {@link text} + */ + readonly globList: readonly { include: boolean; text: string }[]; + + /** + * The user requested to filter for only the specified tags. + */ + readonly onlyTags: ReadonlySet; + + /** + * Focuses the filter input in the test explorer view. + */ focusInput(): void; + + /** + * Replaces the filter {@link text}. + */ + setText(text: string): void; + + /** + * Sets whether the {@link text} is filtering for a special term. + */ + isFilteringFor(term: TestFilterTerm): boolean; + + /** + * Sets whether the {@link text} includes a special filter term. + */ + toggleFilteringFor(term: TestFilterTerm, shouldFilter?: boolean): void; } export const ITestExplorerFilterState = createDecorator('testingFilterState'); +const tagRe = /@([^ ,]*)/g; +const testTagRe = /^@(.+?):(.+)$/; +const trimExtraWhitespace = (str: string) => str.replace(/\s\s+/g, ' ').trim(); + export class TestExplorerFilterState implements ITestExplorerFilterState { declare _serviceBrand: undefined; private readonly focusEmitter = new Emitter(); - public readonly text = new MutableObservableValue(''); - public readonly stateFilter = MutableObservableValue.stored(new StoredValue({ - key: 'testStateFilter', - scope: StorageScope.WORKSPACE, - target: StorageTarget.USER - }, this.storage), TestExplorerStateFilter.All); - public readonly currentDocumentOnly = MutableObservableValue.stored(new StoredValue({ - key: 'testsByCurrentDocumentOnly', - scope: StorageScope.WORKSPACE, - target: StorageTarget.USER - }, this.storage), false); + /** + * Mapping of terms to whether they're included in the text. + */ + private termFilterState: { [K in TestFilterTerm]?: true } = {}; + + /** @inheritdoc */ + public globList: { include: boolean; text: string }[] = []; + + /** @inheritdoc */ + public onlyTags = new Set(); + + /** @inheritdoc */ + public readonly text = new MutableObservableValue(''); - public readonly showExcludedTests = new MutableObservableValue(false); public readonly reveal = new MutableObservableValue(undefined); public readonly onDidRequestInputFocus = this.focusEmitter.event; - constructor(@IStorageService private readonly storage: IStorageService) { } - + /** @inheritdoc */ public focusInput() { this.focusEmitter.fire(); } + + /** @inheritdoc */ + public setText(text: string) { + if (text === this.text.value) { + return; + } + + this.termFilterState = {}; + this.globList = []; + this.onlyTags.clear(); + + let globText = ''; + let lastIndex = 0; + for (const match of text.matchAll(tagRe)) { + globText += text.slice(lastIndex, match.index); + lastIndex = match.index! + match[0].length; + + const tag = match[0]; + if (allTestFilterTerms.includes(tag as TestFilterTerm)) { + this.termFilterState[tag as TestFilterTerm] = true; + } + + const tagMatch = testTagRe.exec(tag); + if (tagMatch) { + this.onlyTags.add(TestTag.namespace(tagMatch[1], tagMatch[2])); + } + } + + globText += text.slice(lastIndex).trim(); + + if (globText.length) { + for (const filter of splitGlobAware(globText, ',').map(s => s.trim()).filter(s => !!s.length)) { + if (filter.startsWith('!')) { + this.globList.push({ include: false, text: filter.slice(1).toLowerCase() }); + } else { + this.globList.push({ include: true, text: filter.toLowerCase() }); + } + } + } + + this.text.value = text; // purposely afterwards so everything is updated when the change event happen + } + + /** @inheritdoc */ + public isFilteringFor(term: TestFilterTerm) { + return !!this.termFilterState[term]; + } + + /** @inheritdoc */ + public toggleFilteringFor(term: TestFilterTerm, shouldFilter?: boolean) { + const text = this.text.value.trim(); + if (shouldFilter !== false && !this.termFilterState[term]) { + this.setText(text ? `${text} ${term}` : term); + } else if (shouldFilter !== true && this.termFilterState[term]) { + this.setText(trimExtraWhitespace(text.replace(term, ''))); + } + } } +export const enum TestFilterTerm { + Failed = '@failed', + Executed = '@executed', + CurrentDoc = '@doc', + Hidden = '@hidden', +} + +const testFilterDescriptions: { [K in TestFilterTerm]: string } = { + [TestFilterTerm.Failed]: localize('testing.filters.showOnlyFailed', "Show Only Failed Tests"), + [TestFilterTerm.Executed]: localize('testing.filters.showOnlyExecuted', "Show Only Executed Tests"), + [TestFilterTerm.CurrentDoc]: localize('testing.filters.currentFile', "Show in Active File Only"), + [TestFilterTerm.Hidden]: localize('testing.filters.showExcludedTests', "Show Hidden Tests"), +}; + +export const allTestFilterTerms = Object.keys(testFilterDescriptions) as readonly TestFilterTerm[]; + export class TestingExplorerFilter extends BaseActionViewItem { - private input!: HistoryInputBox; + private input!: SuggestEnabledInputWithHistory; + private wrapper!: HTMLDivElement; private readonly history: StoredValue = this.instantiationService.createInstance(StoredValue, { - key: 'testing.filterHistory', + key: 'testing.filterHistory2', scope: StorageScope.WORKSPACE, target: StorageTarget.USER }); @@ -86,14 +187,13 @@ export class TestingExplorerFilter extends BaseActionViewItem { constructor( action: IAction, @ITestExplorerFilterState private readonly state: ITestExplorerFilterState, - @IContextViewService private readonly contextViewService: IContextViewService, @IThemeService private readonly themeService: IThemeService, @IInstantiationService private readonly instantiationService: IInstantiationService, + @ITestService private readonly testService: ITestService, ) { super(null, action); this.updateFilterActiveState(); - this._register(state.currentDocumentOnly.onDidChange(this.updateFilterActiveState, this)); - this._register(state.stateFilter.onDidChange(this.updateFilterActiveState, this)); + this._register(testService.excluded.onTestExclusionsChanged(this.updateFilterActiveState, this)); } /** @@ -103,46 +203,68 @@ export class TestingExplorerFilter extends BaseActionViewItem { container.classList.add('testing-filter-action-item'); const updateDelayer = this._register(new Delayer(400)); - const wrapper = dom.$('.testing-filter-wrapper'); + const wrapper = this.wrapper = dom.$('.testing-filter-wrapper'); container.appendChild(wrapper); - const input = this.input = this._register(this.instantiationService.createInstance(ContextScopedHistoryInputBox, wrapper, this.contextViewService, { - placeholder: localize('testExplorerFilter', "Filter (e.g. text, !exclude)"), + const input = this.input = this._register(this.instantiationService.createInstance(ContextScopedSuggestEnabledInputWithHistory, { + id: 'testing.explorer.filter', + ariaLabel: localize('testExplorerFilterLabel', "Filter text for tests in the explorer"), + parent: wrapper, + suggestionProvider: { + triggerCharacters: ['@'], + provideResults: () => [ + ...Object.entries(testFilterDescriptions).map(([label, detail]) => ({ label, detail })), + ...Iterable.map(this.testService.collection.tags.values(), tag => { + const { ctrlId, tagId } = TestTag.denamespace(tag.id); + return ({ + label: `@${ctrlId}:${tagId}`, + detail: tag.label ? `${tag.ctrlLabel} › ${tag.label}` : tag.ctrlLabel, + }); + }), + ].filter(r => !this.state.text.value.includes(r.label)), + } as SuggestResultsProvider, + resourceHandle: 'testing:filter', + suggestOptions: { + value: this.state.text.value, + placeholderText: localize('testExplorerFilter', "Filter (e.g. text, !exclude, @tag)"), + }, history: this.history.get([]), })); - input.value = this.state.text.value; - this._register(attachInputBoxStyler(input, this.themeService)); + this._register(attachSuggestEnabledInputBoxStyler(input, this.themeService)); this._register(this.state.text.onDidChange(newValue => { - input.value = newValue; + if (input.getValue() !== newValue) { + input.setValue(newValue); + } })); this._register(this.state.onDidRequestInputFocus(() => { input.focus(); })); - this._register(input.onDidChange(() => updateDelayer.trigger(() => { + this._register(input.onInputDidChange(() => updateDelayer.trigger(() => { input.addToHistory(); - this.state.text.value = input.value; + this.state.setText(input.getValue()); }))); - this._register(dom.addStandardDisposableListener(input.inputElement, dom.EventType.KEY_DOWN, e => { - if (e.equals(KeyCode.Escape)) { - input.value = ''; - e.stopPropagation(); - e.preventDefault(); - } - })); - const actionbar = this._register(new ActionBar(container, { actionViewItemProvider: action => { if (action.id === this.filtersAction.id) { return this.instantiationService.createInstance(FiltersDropdownMenuActionViewItem, action, this.state, this.actionRunner); } return undefined; - } + }, })); actionbar.push(this.filtersAction, { icon: true, label: false }); + + this.layout(this.wrapper.clientWidth); + } + + public layout(width: number) { + this.input.layout(new dom.Dimension( + width - /* horizontal padding */ 24 - /* editor padding */ 8 - /* filter button padding */ 22, + /* line height */ 27 - /* editor padding */ 4, + )); } @@ -177,8 +299,7 @@ export class TestingExplorerFilter extends BaseActionViewItem { * Updates the 'checked' state of the filter submenu. */ private updateFilterActiveState() { - this.filtersAction.checked = this.state.currentDocumentOnly.value - || this.state.stateFilter.value !== TestExplorerStateFilter.All; + this.filtersAction.checked = this.testService.excluded.hasAny; } } @@ -211,30 +332,24 @@ class FiltersDropdownMenuActionViewItem extends DropdownMenuActionViewItem { private getActions(): IAction[] { return [ - ...[ - { v: TestExplorerStateFilter.OnlyFailed, label: localize('testing.filters.showOnlyFailed', "Show Only Failed Tests") }, - { v: TestExplorerStateFilter.OnlyExecuted, label: localize('testing.filters.showOnlyExecuted', "Show Only Executed Tests") }, - { v: TestExplorerStateFilter.All, label: localize('testing.filters.showAll', "Show All Tests") }, - ].map(({ v, label }) => ({ - checked: this.filters.stateFilter.value === v, + ...[TestFilterTerm.Failed, TestFilterTerm.Executed, TestFilterTerm.CurrentDoc].map(term => ({ + checked: this.filters.isFilteringFor(term), class: undefined, enabled: true, - id: v, - label, - run: async () => { - this.filters.stateFilter.value = this.filters.stateFilter.value === v ? TestExplorerStateFilter.All : v; - }, + id: term, + label: testFilterDescriptions[term], + run: () => this.filters.toggleFilteringFor(term), tooltip: '', dispose: () => null })), new Separator(), { - checked: this.filters.showExcludedTests.value, + checked: this.filters.isFilteringFor(TestFilterTerm.Hidden), class: undefined, - enabled: true, + enabled: this.testService.excluded.hasAny, id: 'showExcluded', label: localize('testing.filters.showExcludedTests', "Show Hidden Tests"), - run: async () => this.filters.showExcludedTests.value = !this.filters.showExcludedTests.value, + run: () => this.filters.toggleFilteringFor(TestFilterTerm.Hidden), tooltip: '', dispose: () => null }, @@ -247,18 +362,7 @@ class FiltersDropdownMenuActionViewItem extends DropdownMenuActionViewItem { run: async () => this.testService.excluded.clear(), tooltip: '', dispose: () => null - }, - new Separator(), - { - checked: this.filters.currentDocumentOnly.value, - class: undefined, - enabled: true, - id: 'currentDocument', - label: localize('testing.filters.currentFile', "Show in Active File Only"), - run: async () => this.filters.currentDocumentOnly.value = !this.filters.currentDocumentOnly.value, - tooltip: '', - dispose: () => null - }, + } ]; } @@ -272,12 +376,6 @@ registerAction2(class extends Action2 { super({ id: Testing.FilterActionId, title: localize('filter', "Filter"), - menu: { - id: MenuId.ViewTitle, - when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', Testing.ExplorerViewId), TestingContextKeys.explorerLocation.isEqualTo(ViewContainerLocation.Panel)), - group: 'navigation', - order: 1, - }, }); } async run(): Promise { } diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts index 0586aad7477..3662bc4ea62 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts @@ -17,7 +17,6 @@ import { Color, RGBA } from 'vs/base/common/color'; import { Emitter, Event } from 'vs/base/common/event'; import * as extpath from 'vs/base/common/extpath'; import { FuzzyScore } from 'vs/base/common/filters'; -import { splitGlobAware } from 'vs/base/common/glob'; import { KeyCode } from 'vs/base/common/keyCodes'; import { Disposable, dispose, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { isDefined } from 'vs/base/common/types'; @@ -46,16 +45,16 @@ import { IThemeService, registerThemingParticipant, ThemeIcon } from 'vs/platfor import { IResourceLabel, IResourceLabelOptions, IResourceLabelProps, ResourceLabels } from 'vs/workbench/browser/labels'; import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; -import { IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views'; +import { IViewDescriptorService } from 'vs/workbench/common/views'; import { HierarchicalByLocationProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation'; import { ByNameTestItemElement, HierarchicalByNameProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName'; import { ITestTreeProjection, TestExplorerTreeElement, TestItemTreeElement, TestTreeErrorMessage } from 'vs/workbench/contrib/testing/browser/explorerProjections/index'; import { getTestItemContextOverlay } from 'vs/workbench/contrib/testing/browser/explorerProjections/testItemContextOverlay'; import * as icons from 'vs/workbench/contrib/testing/browser/icons'; -import { ITestExplorerFilterState, TestExplorerFilterState, TestingExplorerFilter } from 'vs/workbench/contrib/testing/browser/testingExplorerFilter'; +import { ITestExplorerFilterState, TestExplorerFilterState, TestFilterTerm, TestingExplorerFilter } from 'vs/workbench/contrib/testing/browser/testingExplorerFilter'; import { ITestingProgressUiService } from 'vs/workbench/contrib/testing/browser/testingProgressUiService'; import { getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration'; -import { labelForTestInState, TestExplorerStateFilter, TestExplorerViewMode, TestExplorerViewSorting, Testing, testStateNames } from 'vs/workbench/contrib/testing/common/constants'; +import { labelForTestInState, TestExplorerViewMode, TestExplorerViewSorting, Testing, testStateNames } from 'vs/workbench/contrib/testing/common/constants'; import { InternalTestItem, ITestRunProfile, TestItemExpandState, TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; @@ -74,7 +73,7 @@ export class TestingExplorerView extends ViewPane { private container!: HTMLElement; private treeHeader!: HTMLElement; private discoveryProgress = this._register(new MutableDisposable()); - private readonly location = TestingContextKeys.explorerLocation.bindTo(this.contextKeyService); + private filter?: TestingExplorerFilter; private readonly dimensions = { width: 0, height: 0 }; constructor( @@ -94,7 +93,6 @@ export class TestingExplorerView extends ViewPane { @ICommandService private readonly commandService: ICommandService, ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); - this.location.set(viewDescriptorService.getViewLocationById(Testing.ExplorerViewId) ?? ViewContainerLocation.Sidebar); const relayout = this._register(new RunOnceScheduler(() => this.layoutBody(), 1)); this._register(this.onDidChangeViewWelcomeState(() => { @@ -211,10 +209,7 @@ export class TestingExplorerView extends ViewPane { this.container = dom.append(container, dom.$('.test-explorer')); this.treeHeader = dom.append(this.container, dom.$('.test-explorer-header')); - - if (this.location.get() === ViewContainerLocation.Sidebar) { - this.filterActionBar.value = this.createFilterActionBar(); - } + this.filterActionBar.value = this.createFilterActionBar(); const messagesContainer = dom.append(this.treeHeader, dom.$('.test-explorer-messages')); this._register(this.testProgressService.onTextChange(text => { @@ -250,7 +245,7 @@ export class TestingExplorerView extends ViewPane { public override getActionViewItem(action: IAction): IActionViewItem | undefined { switch (action.id) { case Testing.FilterActionId: - return this.instantiationService.createInstance(TestingExplorerFilter, action); + return this.filter = this.instantiationService.createInstance(TestingExplorerFilter, action); case RunSelectedAction.ID: return this.getRunGroupDropdown(TestRunProfileBitset.Run, action); case DebugSelectedAction.ID: @@ -336,6 +331,7 @@ export class TestingExplorerView extends ViewPane { * @override */ public override saveState() { + this.filter?.saveState(); super.saveState(); } @@ -391,6 +387,7 @@ export class TestingExplorerView extends ViewPane { this.dimensions.width = width; this.container.style.height = `${height}px`; this.viewModel.layout(height - this.treeHeader.clientHeight, width); + this.filter?.layout(width); } } @@ -520,9 +517,6 @@ export class TestingExplorerViewModel extends Disposable { this._register(Event.any( filterState.text.onDidChange, - filterState.stateFilter.onDidChange, - filterState.showExcludedTests.onDidChange, - filterState.currentDocumentOnly.onDidChange, testService.excluded.onTestExclusionsChanged, )(this.tree.refilter, this.tree)); @@ -599,19 +593,9 @@ export class TestingExplorerViewModel extends Disposable { this.tree.rerender(); })); - this._register(filterState.currentDocumentOnly.onDidChange(() => { - if (!filterState.currentDocumentOnly.value) { - this.filter.filterToDocumentUri(undefined); - } else if (editorService.activeEditor?.resource) { - this.filter.filterToDocumentUri(editorService.activeEditor.resource); - } - - this.tree.refilter(); - })); - const onEditorChange = () => { - if (filterState.currentDocumentOnly.value && editorService.activeEditor?.resource) { - this.filter.filterToDocumentUri(editorService.activeEditor.resource); + this.filter.filterToDocumentUri(editorService.activeEditor?.resource); + if (this.filterState.isFilteringFor(TestFilterTerm.CurrentDoc)) { this.tree.refilter(); } }; @@ -669,7 +653,7 @@ export class TestingExplorerViewModel extends Disposable { // excluded tests' checkbox automatically. for (let n: TestItemTreeElement | null = element; n instanceof TestItemTreeElement; n = n.parent) { if (n.test && this.testService.excluded.contains(n.test)) { - this.filterState.showExcludedTests.value = true; + this.filterState.toggleFilteringFor(TestFilterTerm.Hidden, true); break; } } @@ -760,7 +744,7 @@ export class TestingExplorerViewModel extends Disposable { private reevaluateWelcomeState() { const shouldShowWelcome = this.testService.collection.busyProviders === 0 && testCollectionIsEmpty(this.testService.collection); const welcomeExperience = shouldShowWelcome - ? (this.filterState.currentDocumentOnly.value ? WelcomeExperience.ForDocument : WelcomeExperience.ForWorkspace) + ? (this.filterState.isFilteringFor(TestFilterTerm.CurrentDoc) ? WelcomeExperience.ForDocument : WelcomeExperience.ForWorkspace) : WelcomeExperience.None; if (welcomeExperience !== this.welcomeExperience) { @@ -845,8 +829,6 @@ const hasNodeInOrParentOfUri = (collection: IMainThreadTestCollection, testUri: }; class TestsFilter implements ITreeFilter { - private lastText?: string; - private filters: [include: boolean, value: string][] | undefined; private documentUri: URI | undefined; constructor( @@ -855,28 +837,6 @@ class TestsFilter implements ITreeFilter { @ITestService private readonly testService: ITestService, ) { } - /** - * Parses and updates the tree filter. Supports lists of patterns that can be !negated. - */ - private setFilter(text: string) { - this.lastText = text; - text = text.trim(); - - if (!text) { - this.filters = undefined; - return; - } - - this.filters = []; - for (const filter of splitGlobAware(text, ',').map(s => s.trim()).filter(s => !!s.length)) { - if (filter.startsWith('!')) { - this.filters.push([false, filter.slice(1).toLowerCase()]); - } else { - this.filters.push([true, filter.toLowerCase()]); - } - } - } - /** * @inheritdoc */ @@ -885,19 +845,15 @@ class TestsFilter implements ITreeFilter { return TreeVisibility.Visible; } - if (this.state.text.value !== this.lastText) { - this.setFilter(this.state.text.value); - } - if ( element.test - && !this.state.showExcludedTests.value + && !this.state.isFilteringFor(TestFilterTerm.Hidden) && this.testService.excluded.contains(element.test) ) { return TreeVisibility.Hidden; } - switch (Math.min(this.testFilterText(element), this.testLocation(element), this.testState(element))) { + switch (Math.min(this.testFilterText(element), this.testLocation(element), this.testState(element), this.testTags(element))) { case FilterResult.Exclude: return TreeVisibility.Hidden; case FilterResult.Include: @@ -911,15 +867,26 @@ class TestsFilter implements ITreeFilter { this.documentUri = uri; } - private testState(element: TestItemTreeElement): FilterResult { - switch (this.state.stateFilter.value) { - case TestExplorerStateFilter.All: - return FilterResult.Include; - case TestExplorerStateFilter.OnlyExecuted: - return element.state !== TestResultState.Unset ? FilterResult.Include : FilterResult.Inherit; - case TestExplorerStateFilter.OnlyFailed: - return isFailedState(element.state) ? FilterResult.Include : FilterResult.Inherit; + private testTags(element: TestItemTreeElement): FilterResult { + if (!this.state.onlyTags.size) { + return FilterResult.Include; } + + return element.test.item.tags.some(t => this.state.onlyTags.has(t)) + ? FilterResult.Include + : FilterResult.Inherit; + } + + private testState(element: TestItemTreeElement): FilterResult { + if (this.state.isFilteringFor(TestFilterTerm.Failed)) { + return isFailedState(element.state) ? FilterResult.Include : FilterResult.Inherit; + } + + if (this.state.isFilteringFor(TestFilterTerm.Executed)) { + return element.state !== TestResultState.Unset ? FilterResult.Include : FilterResult.Inherit; + } + + return FilterResult.Include; } private testLocation(element: TestItemTreeElement): FilterResult { @@ -927,7 +894,7 @@ class TestsFilter implements ITreeFilter { return FilterResult.Include; } - if (!this.state.currentDocumentOnly.value || !(element instanceof TestItemTreeElement)) { + if (!this.state.isFilteringFor(TestFilterTerm.CurrentDoc) || !(element instanceof TestItemTreeElement)) { return FilterResult.Include; } @@ -936,17 +903,17 @@ class TestsFilter implements ITreeFilter { } private testFilterText(element: TestItemTreeElement) { - if (!this.filters) { + if (this.state.globList.length === 0) { return FilterResult.Include; } for (let e: TestItemTreeElement | null = element; e; e = e.parent) { // start as included if the first glob is a negation - let included = this.filters[0][0] === false ? FilterResult.Include : FilterResult.Inherit; + let included = this.state.globList[0].include === false ? FilterResult.Include : FilterResult.Inherit; const data = e.label.toLowerCase(); - for (const [include, filter] of this.filters) { - if (data.includes(filter)) { + for (const { include, text } of this.state.globList) { + if (data.includes(text)) { included = include ? FilterResult.Include : FilterResult.Exclude; } } @@ -999,7 +966,7 @@ class NoTestsForDocumentWidget extends Disposable { const button = this._register(new Button(el, { title: buttonLabel })); button.label = buttonLabel; this._register(attachButtonStyler(button, themeService)); - this._register(button.onDidClick(() => filterState.currentDocumentOnly.value = false)); + this._register(button.onDidClick(() => filterState.toggleFilteringFor(TestFilterTerm.CurrentDoc, false))); } public setVisible(isVisible: boolean) { diff --git a/src/vs/workbench/contrib/testing/common/ownedTestCollection.ts b/src/vs/workbench/contrib/testing/common/ownedTestCollection.ts index bda407e4f34..4df54697d40 100644 --- a/src/vs/workbench/contrib/testing/common/ownedTestCollection.ts +++ b/src/vs/workbench/contrib/testing/common/ownedTestCollection.ts @@ -46,6 +46,8 @@ export class SingleUseTestCollection extends Disposable { public readonly root = new TestItemRootImpl(this.controllerId, this.controllerId); public readonly tree = new Map(); + private readonly tags = new Map(); + protected diff: TestsDiff = []; constructor(private readonly controllerId: string) { @@ -78,21 +80,6 @@ export class SingleUseTestCollection extends Disposable { return diff; } - /** - * Gets all tags associated with items in the collection. - */ - public *tags() { - const seen = new Set(); - for (const item of this.tree.values()) { - for (const tag of item.actual.tags) { - if (!seen.has(tag.id)) { - seen.add(tag.id); - yield tag; - } - } - } - } - /** * Pushes a new diff entry onto the collected diff list. */ @@ -178,14 +165,14 @@ export class SingleUseTestCollection extends Disposable { break; case ExtHostTestItemEventOp.SetProp: - const { key, value } = evt; + const { key, value, previous } = evt; const extId = internal.fullId.toString(); switch (key) { case 'canResolveChildren': this.updateExpandability(internal); break; case 'tags': - this.pushDiff([TestDiffOpType.Update, { extId, item: { tags: (value as ITestTag[]).map(v => v.id) }, }]); + this.diffTagRefs(value, previous, extId); break; case 'range': this.pushDiff([TestDiffOpType.Update, { extId, item: { range: Convert.Range.from(value) }, }]); @@ -228,6 +215,7 @@ export class SingleUseTestCollection extends Disposable { expand: TestItemExpandState.NotExpandable, // updated by `connectItemAndChildren` }; + actual.tags.forEach(this.incrementTagRefs, this); this.tree.set(internal.fullId.toString(), internal); this.setItemParent(actual, parent); this.pushDiff([ @@ -259,7 +247,7 @@ export class SingleUseTestCollection extends Disposable { internal.actual = actual; internal.expand = TestItemExpandState.NotExpandable; // updated by `connectItemAndChildren` for (const [key, value] of changedProps) { - this.onTestItemEvent(internal, { op: ExtHostTestItemEventOp.SetProp, key, value }); + this.onTestItemEvent(internal, { op: ExtHostTestItemEventOp.SetProp, key, value, previous: oldActual[key] }); } this.connectItemAndChildren(actual, internal, parent); @@ -272,6 +260,44 @@ export class SingleUseTestCollection extends Disposable { } } + private diffTagRefs(newTags: ITestTag[], oldTags: ITestTag[], extId: string) { + const toDelete = new Set(oldTags.map(t => t.id)); + for (const tag of newTags) { + if (!toDelete.delete(tag.id)) { + this.incrementTagRefs(tag); + } + } + + this.pushDiff([ + TestDiffOpType.Update, + { extId, item: { tags: newTags.map(v => Convert.TestTag.namespace(this.controllerId, v.id)) } }] + ); + + toDelete.forEach(this.decrementTagRefs, this); + } + + private incrementTagRefs(tag: ITestTag) { + const existing = this.tags.get(tag.id); + if (existing) { + existing.refCount++; + } else { + this.tags.set(tag.id, { label: tag.label, refCount: 1 }); + this.pushDiff([TestDiffOpType.AddTag, { + id: Convert.TestTag.namespace(this.controllerId, tag.id), + ctrlLabel: this.root.label, + label: tag.label, + }]); + } + } + + private decrementTagRefs(tagId: string) { + const existing = this.tags.get(tagId); + if (existing && !--existing.refCount) { + this.tags.delete(tagId); + this.pushDiff([TestDiffOpType.RemoveTag, Convert.TestTag.namespace(this.controllerId, tagId)]); + } + } + private setItemParent(actual: TestItemImpl, parent: OwnedCollectionTestItem | undefined) { getPrivateApiFor(actual).parent = parent && parent.actual !== this.root ? parent.actual : undefined; } @@ -412,6 +438,11 @@ export class SingleUseTestCollection extends Disposable { } getPrivateApiFor(item.actual).listener = undefined; + + for (const tag of item.actual.tags) { + this.decrementTagRefs(tag.id); + } + this.tree.delete(item.fullId.toString()); for (const child of item.actual.children) { queue.push(this.tree.get(TestId.joinToString(item.fullId, child.id))); diff --git a/src/vs/workbench/contrib/testing/common/testCollection.ts b/src/vs/workbench/contrib/testing/common/testCollection.ts index 51b12227d3f..c7bc238a79b 100644 --- a/src/vs/workbench/contrib/testing/common/testCollection.ts +++ b/src/vs/workbench/contrib/testing/common/testCollection.ts @@ -137,7 +137,7 @@ export interface ITestTag { export interface ITestTagDisplayInfo { id: string; - displayId: string; + ctrlLabel: string; label?: string; } @@ -286,6 +286,10 @@ export const enum TestDiffOpType { IncrementPendingExtHosts, /** Retires a test/result */ Retire, + /** Add a new test tag */ + AddTag, + /** Remove a test tag */ + RemoveTag, } export type TestsDiffOp = @@ -293,7 +297,9 @@ export type TestsDiffOp = | [op: TestDiffOpType.Update, item: ITestItemUpdate] | [op: TestDiffOpType.Remove, itemId: string] | [op: TestDiffOpType.Retire, itemId: string] - | [op: TestDiffOpType.IncrementPendingExtHosts, amount: number]; + | [op: TestDiffOpType.IncrementPendingExtHosts, amount: number] + | [op: TestDiffOpType.AddTag, tag: ITestTagDisplayInfo] + | [op: TestDiffOpType.RemoveTag, id: string]; /** * Context for actions taken in the test explorer view. @@ -351,6 +357,8 @@ export class IncrementalChangeCollector { * Maintains tests in this extension host sent from the main thread. */ export abstract class AbstractIncrementalTestCollection { + private readonly _tags = new Map(); + /** * Map of item IDs to test item objects. */ @@ -371,6 +379,11 @@ export abstract class AbstractIncrementalTestCollection = this._tags; + /** * Applies the diff to the collection. */ @@ -459,6 +472,14 @@ export abstract class AbstractIncrementalTestCollection('testService'); export interface IMainThreadTestController { readonly id: string; readonly label: IObservableValue; - getTags(): Promise; configureRunProfile(profileId: number): void; expandTest(id: string, levels: number): Promise; runTests(request: RunTestForControllerRequest, token: CancellationToken): Promise; diff --git a/src/vs/workbench/contrib/testing/common/testingContextKeys.ts b/src/vs/workbench/contrib/testing/common/testingContextKeys.ts index 03f4344112b..9a6a8e93a1e 100644 --- a/src/vs/workbench/contrib/testing/common/testingContextKeys.ts +++ b/src/vs/workbench/contrib/testing/common/testingContextKeys.ts @@ -5,7 +5,6 @@ import { localize } from 'vs/nls'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { ViewContainerLocation } from 'vs/workbench/common/views'; import { TestExplorerViewMode, TestExplorerViewSorting } from 'vs/workbench/contrib/testing/common/constants'; import { TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection'; @@ -31,7 +30,6 @@ export namespace TestingContextKeys { export const isRunning = new RawContextKey('testing.isRunning', false); export const isInPeek = new RawContextKey('testing.isInPeek', true); export const isPeekVisible = new RawContextKey('testing.isPeekVisible', false); - export const explorerLocation = new RawContextKey('testing.explorerLocation', ViewContainerLocation.Sidebar); export const autoRun = new RawContextKey('testing.autoRun', false); export const peekItemType = new RawContextKey('peekItemType', undefined, { diff --git a/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts b/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts index b0a3ecc756f..d878ddc4252 100644 --- a/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts +++ b/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts @@ -5,26 +5,26 @@ /* eslint-disable code-import-patterns */ -import { URI } from 'vs/base/common/uri'; -import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; -import { Event } from 'vs/base/common/event'; -import { IAddressProvider } from 'vs/platform/remote/common/remoteAgentConnection'; -import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { IExtensionService, NullExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { isWindows } from 'vs/base/common/platform'; -import { ITunnelProvider, ITunnelService, RemoteTunnel, TunnelProviderFeatures } from 'vs/platform/remote/common/tunnel'; -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; -import { joinPath } from 'vs/base/common/resources'; import { VSBuffer } from 'vs/base/common/buffer'; -import { SearchService } from 'vs/workbench/services/search/common/searchService'; -import { ISearchService } from 'vs/workbench/services/search/common/search'; +import { Event } from 'vs/base/common/event'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { isWindows } from 'vs/base/common/platform'; +import { joinPath } from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IFileService } from 'vs/platform/files/common/files'; +import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; +import { IAddressProvider } from 'vs/platform/remote/common/remoteAgentConnection'; +import { ITunnelProvider, ITunnelService, RemoteTunnel, TunnelProviderFeatures } from 'vs/platform/remote/common/tunnel'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; +import { IExtensionService, NullExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { ISearchService } from 'vs/workbench/services/search/common/search'; +import { SearchService } from 'vs/workbench/services/search/common/searchService'; +import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; //#region Environment diff --git a/src/vs/workbench/services/search/common/search.ts b/src/vs/workbench/services/search/common/search.ts index f5f670c7956..32cb38af55f 100644 --- a/src/vs/workbench/services/search/common/search.ts +++ b/src/vs/workbench/services/search/common/search.ts @@ -18,6 +18,7 @@ import { Event } from 'vs/base/common/event'; import * as paths from 'vs/base/common/path'; import { isPromiseCanceledError } from 'vs/base/common/errors'; import { TextSearchCompleteMessageType } from 'vs/workbench/services/search/common/searchExtTypes'; +import { isPromise } from 'vs/base/common/types'; export { TextSearchCompleteMessageType }; @@ -655,22 +656,29 @@ export class QueryGlobTester { } /** - * Guaranteed async. + * Evaluating the exclude expression is only async if it includes sibling clauses. As an optimization, avoid doing anything with Promises + * unless the expression is async. */ - includedInQuery(testPath: string, basename?: string, hasSibling?: (name: string) => boolean | Promise): Promise { - const excludeP = Promise.resolve(this._parsedExcludeExpression(testPath, basename, hasSibling)).then(result => !!result); - - return excludeP.then(excluded => { - if (excluded) { - return false; - } + includedInQuery(testPath: string, basename?: string, hasSibling?: (name: string) => boolean | Promise): Promise | boolean { + const excluded = this._parsedExcludeExpression(testPath, basename, hasSibling); + const isIncluded = () => { return this._parsedIncludeExpression ? - Promise.resolve(this._parsedIncludeExpression(testPath, basename, hasSibling)).then(result => !!result) : - Promise.resolve(true); - }).then(included => { - return included; - }); + !!(this._parsedIncludeExpression(testPath, basename, hasSibling)) : + true; + }; + + if (isPromise(excluded)) { + return excluded.then(excluded => { + if (excluded) { + return false; + } + + return isIncluded(); + }); + } + + return isIncluded(); } hasSiblingExcludeClauses(): boolean { diff --git a/src/vs/workbench/services/search/common/textSearchManager.ts b/src/vs/workbench/services/search/common/textSearchManager.ts index 30df394a8a9..de7d5487027 100644 --- a/src/vs/workbench/services/search/common/textSearchManager.ts +++ b/src/vs/workbench/services/search/common/textSearchManager.ts @@ -3,17 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as path from 'vs/base/common/path'; import { flatten, mapArrayOrNot } from 'vs/base/common/arrays'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { toErrorMessage } from 'vs/base/common/errorMessage'; -import * as resources from 'vs/base/common/resources'; import * as glob from 'vs/base/common/glob'; -import { URI } from 'vs/base/common/uri'; -import { IExtendedExtensionSearchOptions, IFileMatch, IFolderQuery, IPatternInfo, ISearchCompleteStats, ITextQuery, ITextSearchContext, ITextSearchMatch, ITextSearchResult, QueryGlobTester, resolvePatternsForProvider } from 'vs/workbench/services/search/common/search'; -import { TextSearchProvider, TextSearchResult, TextSearchMatch, TextSearchComplete, Range, TextSearchOptions, TextSearchQuery } from 'vs/workbench/services/search/common/searchExtTypes'; import { Schemas } from 'vs/base/common/network'; -import { isArray } from 'vs/base/common/types'; +import * as path from 'vs/base/common/path'; +import * as resources from 'vs/base/common/resources'; +import { isArray, isPromise } from 'vs/base/common/types'; +import { URI } from 'vs/base/common/uri'; +import { IExtendedExtensionSearchOptions, IFileMatch, IFolderQuery, IPatternInfo, ISearchCompleteStats, ITextQuery, ITextSearchContext, ITextSearchMatch, ITextSearchResult, ITextSearchStats, QueryGlobTester, resolvePatternsForProvider } from 'vs/workbench/services/search/common/search'; +import { Range, TextSearchComplete, TextSearchMatch, TextSearchOptions, TextSearchProvider, TextSearchQuery, TextSearchResult } from 'vs/workbench/services/search/common/searchExtTypes'; export interface IFileUtils { readdir: (resource: URI) => Promise; @@ -27,7 +27,7 @@ export class TextSearchManager { private isLimitHit = false; private resultCount = 0; - constructor(private query: ITextQuery, private provider: TextSearchProvider, private fileUtils: IFileUtils) { } + constructor(private query: ITextQuery, private provider: TextSearchProvider, private fileUtils: IFileUtils, private processType: ITextSearchStats['type']) { } search(onProgress: (matches: IFileMatch[]) => void, token: CancellationToken): Promise { const folderQueries = this.query.folderQueries || []; @@ -77,7 +77,7 @@ export class TextSearchManager { else { return [result.message]; } })), stats: { - type: 'textSearchProvider' + type: this.processType } }); }, (err: Error) => { @@ -131,20 +131,28 @@ export class TextSearchManager { const relativePath = resources.relativePath(folderQuery.folder, result.uri); if (relativePath) { - testingPs.push( - queryTester.includedInQuery(relativePath, path.basename(relativePath), hasSibling) - .then(included => { - if (included) { + // This method is only async when the exclude contains sibling clauses + const included = queryTester.includedInQuery(relativePath, path.basename(relativePath), hasSibling); + if (isPromise(included)) { + testingPs.push( + included.then(isIncluded => { + if (isIncluded) { onResult(result); } })); + } else if (included) { + onResult(result); + } } } }; const searchOptions = this.getSearchOptionsForFolder(folderQuery); const result = await this.provider.provideTextSearchResults(patternInfoToQuery(this.query.contentPattern), searchOptions, progress, token); - await Promise.all(testingPs); + if (testingPs.length) { + await Promise.all(testingPs); + } + return result; } diff --git a/src/vs/workbench/services/search/electron-browser/searchService.ts b/src/vs/workbench/services/search/electron-browser/searchService.ts index 3e742a28ace..a58b4eb26f6 100644 --- a/src/vs/workbench/services/search/electron-browser/searchService.ts +++ b/src/vs/workbench/services/search/electron-browser/searchService.ts @@ -40,6 +40,7 @@ export class LocalSearchService extends SearchService { @INativeWorkbenchEnvironmentService readonly environmentService: INativeWorkbenchEnvironmentService, @IInstantiationService readonly instantiationService: IInstantiationService, @IUriIdentityService uriIdentityService: IUriIdentityService, + @IConfigurationService configurationService: IConfigurationService, ) { super(modelService, editorService, telemetryService, logService, extensionService, fileService, uriIdentityService); diff --git a/src/vs/workbench/services/search/node/rawSearchService.ts b/src/vs/workbench/services/search/node/rawSearchService.ts index f5e19bdd076..4e8a5782041 100644 --- a/src/vs/workbench/services/search/node/rawSearchService.ts +++ b/src/vs/workbench/services/search/node/rawSearchService.ts @@ -30,6 +30,8 @@ export class SearchService implements IRawSearchService { private caches: { [cacheKey: string]: Cache; } = Object.create(null); + constructor(private readonly processType: IFileSearchStats['type'] = 'searchProcess') { } + fileSearch(config: IRawFileQuery): Event { let promise: CancelablePromise; @@ -124,7 +126,7 @@ export class SearchService implements IRawSearchService { type: 'success', stats: { detailStats: complete.stats, - type: 'searchProcess', + type: this.processType, fromCache: false, resultCount, sortingTime: undefined @@ -191,7 +193,7 @@ export class SearchService implements IRawSearchService { detailStats: result.stats, sortingTime, fromCache: false, - type: 'searchProcess', + type: this.processType, workspaceFolderCount: config.folderQueries.length, resultCount: sortedResults.length }, @@ -226,7 +228,7 @@ export class SearchService implements IRawSearchService { const stats: IFileSearchStats = { fromCache: true, detailStats: cacheStats, - type: 'searchProcess', + type: this.processType, resultCount: results.length, sortingTime }; diff --git a/src/vs/workbench/services/search/node/textSearchAdapter.ts b/src/vs/workbench/services/search/node/textSearchAdapter.ts index 5d7c484e3ca..97c8af5e69b 100644 --- a/src/vs/workbench/services/search/node/textSearchAdapter.ts +++ b/src/vs/workbench/services/search/node/textSearchAdapter.ts @@ -38,7 +38,7 @@ export class TextSearchEngineAdapter { }, token) .then( - c => resolve({ limitHit: c.limitHit, type: 'success' } as ISerializedSearchSuccess), + c => resolve({ limitHit: c.limitHit, type: 'success', stats: c.stats } as ISerializedSearchSuccess), reject); }); } diff --git a/src/vs/workbench/services/search/node/textSearchManager.ts b/src/vs/workbench/services/search/node/textSearchManager.ts index 19171775f7a..42cb5e59e80 100644 --- a/src/vs/workbench/services/search/node/textSearchManager.ts +++ b/src/vs/workbench/services/search/node/textSearchManager.ts @@ -5,16 +5,16 @@ import { toCanonicalName } from 'vs/workbench/services/textfile/common/encoding'; import * as pfs from 'vs/base/node/pfs'; -import { ITextQuery } from 'vs/workbench/services/search/common/search'; +import { ITextQuery, ITextSearchStats } from 'vs/workbench/services/search/common/search'; import { TextSearchProvider } from 'vs/workbench/services/search/common/searchExtTypes'; import { TextSearchManager } from 'vs/workbench/services/search/common/textSearchManager'; export class NativeTextSearchManager extends TextSearchManager { - constructor(query: ITextQuery, provider: TextSearchProvider, _pfs: typeof pfs = pfs) { + constructor(query: ITextQuery, provider: TextSearchProvider, _pfs: typeof pfs = pfs, processType: ITextSearchStats['type'] = 'searchProcess') { super(query, provider, { readdir: resource => _pfs.Promises.readdir(resource.fsPath), toCanonicalName: name => toCanonicalName(name) - }); + }, processType); } } diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index 654054bd872..3167e1b184f 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -25,8 +25,8 @@ import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cance import { UTF8 } 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 { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; +import { extUri } from 'vs/base/common/resources'; interface IBackupMetaData extends IWorkingCopyBackupMeta { mtime: number; @@ -77,6 +77,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil readonly capabilities = WorkingCopyCapabilities.None; readonly name = basename(this.labelService.getUriLabel(this.resource)); + private resourceHasExtension: boolean = !!extUri.extname(this.resource); private contentEncoding: string | undefined; // encoding as reported from disk @@ -607,10 +608,9 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } protected override async autoDetectLanguage(): Promise { - const mode = this.getMode(); if ( - this.resource.scheme === this.pathService.defaultUriScheme && // make sure to not detect language for non-user visible documents - (!mode || mode === PLAINTEXT_MODE_ID) // require a valid mode that is enlisted for detection + this.resource.scheme === this.pathService.defaultUriScheme && // make sure to not detect language for non-user visible documents + !this.resourceHasExtension // only run if this particular file doesn't have an extension ) { return super.autoDetectLanguage(); } diff --git a/src/vs/workbench/test/browser/api/extHostTesting.test.ts b/src/vs/workbench/test/browser/api/extHostTesting.test.ts index 8bc33ad3f8a..de080ec74a8 100644 --- a/src/vs/workbench/test/browser/api/extHostTesting.test.ts +++ b/src/vs/workbench/test/browser/api/extHostTesting.test.ts @@ -9,9 +9,9 @@ import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Iterable } from 'vs/base/common/iterator'; import { mockObject, MockObject } from 'vs/base/test/common/mock'; import { MainThreadTestingShape } from 'vs/workbench/api/common/extHost.protocol'; -import { TestRunProfileImpl, TestRunCoordinator, TestRunDto } from 'vs/workbench/api/common/extHostTesting'; +import { TestRunCoordinator, TestRunDto, TestRunProfileImpl } from 'vs/workbench/api/common/extHostTesting'; import * as convert from 'vs/workbench/api/common/extHostTypeConverters'; -import { TestMessage, TestResultState, TestRunProfileKind } from 'vs/workbench/api/common/extHostTypes'; +import { TestMessage, TestResultState, TestRunProfileKind, TestTag } from 'vs/workbench/api/common/extHostTypes'; import { TestDiffOpType, TestItemExpandState } from 'vs/workbench/contrib/testing/common/testCollection'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { TestItemImpl, testStubs } from 'vs/workbench/contrib/testing/common/testStubs'; @@ -171,6 +171,45 @@ suite('ExtHost Testing', () => { assert.strictEqual(single.tree.size, 6); }); + test('manages tags correctly', () => { + single.expand(single.root.id, Infinity); + single.collectDiff(); + const tag1 = new TestTag('tag1', 'Tag 1'); + const tag2 = new TestTag('tag2', 'Tag 2'); + const tag3 = new TestTag('tag3'); + const child = new TestItemImpl('ctrlId', 'id-ac', 'c', undefined); + child.tags = [tag1, tag2]; + single.root.children.get('id-a')!.children.add(child); + + assert.deepStrictEqual(single.collectDiff(), [ + [TestDiffOpType.AddTag, { ctrlLabel: 'root', id: 'ctrlId\0tag1', label: 'Tag 1' }], + [TestDiffOpType.AddTag, { ctrlLabel: 'root', id: 'ctrlId\0tag2', label: 'Tag 2' }], + [TestDiffOpType.Add, { + controllerId: 'ctrlId', + parent: new TestId(['ctrlId', 'id-a']).toString(), + expand: TestItemExpandState.NotExpandable, + item: convert.TestItem.from(child), + }], + ]); + + child.tags = [tag2, tag3]; + assert.deepStrictEqual(single.collectDiff(), [ + [TestDiffOpType.AddTag, { ctrlLabel: 'root', id: 'ctrlId\0tag3', label: undefined }], + [TestDiffOpType.Update, { + extId: new TestId(['ctrlId', 'id-a', 'id-ac']).toString(), + item: { tags: ['ctrlId\0tag2', 'ctrlId\0tag3'] } + }], + [TestDiffOpType.RemoveTag, 'ctrlId\0tag1'], + ]); + + const a = single.root.children.get('id-a')!; + a.tags = [tag2]; + a.children.replace([]); + assert.deepStrictEqual(single.collectDiff().filter(t => t[0] === TestDiffOpType.RemoveTag), [ + [TestDiffOpType.RemoveTag, 'ctrlId\0tag3'], + ]); + }); + test('treats in-place replacement as mutation', () => { single.expand(single.root.id, Infinity); single.collectDiff(); diff --git a/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts b/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts index c5e6b2db736..af580476412 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts @@ -5,25 +5,25 @@ import * as assert from 'assert'; import { mapArrayOrNot } from 'vs/base/common/arrays'; +import { timeout } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { isPromiseCanceledError } from 'vs/base/common/errors'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { joinPath } from 'vs/base/common/resources'; import { URI, UriComponents } from 'vs/base/common/uri'; import * as pfs from 'vs/base/node/pfs'; -import { MainContext, MainThreadSearchShape } from 'vs/workbench/api/common/extHost.protocol'; -import { NativeExtHostSearch } from 'vs/workbench/api/node/extHostSearch'; -import { Range } from 'vs/workbench/api/common/extHostTypes'; -import { IFileMatch, IFileQuery, IPatternInfo, IRawFileMatch2, ISearchCompleteStats, ISearchQuery, ITextQuery, QueryType, resultIsMatch } from 'vs/workbench/services/search/common/search'; -import { TestRPCProtocol } from 'vs/workbench/test/browser/api/testRPCProtocol'; -import type * as vscode from 'vscode'; -import { NullLogService } from 'vs/platform/log/common/log'; -import { URITransformerService } from 'vs/workbench/api/common/extHostUriTransformerService'; import { mock } from 'vs/base/test/common/mock'; +import { NullLogService } from 'vs/platform/log/common/log'; +import { MainContext, MainThreadSearchShape } from 'vs/workbench/api/common/extHost.protocol'; import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; +import { Range } from 'vs/workbench/api/common/extHostTypes'; +import { URITransformerService } from 'vs/workbench/api/common/extHostUriTransformerService'; +import { NativeExtHostSearch } from 'vs/workbench/api/node/extHostSearch'; +import { IFileMatch, IFileQuery, IPatternInfo, IRawFileMatch2, ISearchCompleteStats, ISearchQuery, ITextQuery, QueryType, resultIsMatch } from 'vs/workbench/services/search/common/search'; import { TextSearchManager } from 'vs/workbench/services/search/common/textSearchManager'; import { NativeTextSearchManager } from 'vs/workbench/services/search/node/textSearchManager'; -import { timeout } from 'vs/base/common/async'; +import { TestRPCProtocol } from 'vs/workbench/test/browser/api/testRPCProtocol'; +import type * as vscode from 'vscode'; let rpcProtocol: TestRPCProtocol; let extHostSearch: NativeExtHostSearch; diff --git a/yarn.lock b/yarn.lock index 80babfe5729..bf3378c3e3b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11112,20 +11112,20 @@ xterm-addon-unicode11@0.3.0-beta.6: resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.3.0-beta.6.tgz#8914f377757d5078e7b4daee7d3e2b7428b6edf0" integrity sha512-Qwa18yMhtacf9Jtxy+UuxHfjIeIjaX9q0LUfHtZU8/Lwjh+bGcn8E8IABVSGvXZgPNKw/4TqEpgLFexn+sfc5g== -xterm-addon-webgl@0.12.0-beta.8: - version "0.12.0-beta.8" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.12.0-beta.8.tgz#c11cc8f9065986efe48422b2bfc8427403f30d5f" - integrity sha512-T6qQlol20ezuTdEC+iwuxNFZfCej7thBqKGA5mTBXSDpXoWUw//h/f8iFU0lqNNAI0zgqIPulIiRRJRTUVM8pA== +xterm-addon-webgl@0.12.0-beta.10: + version "0.12.0-beta.10" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.12.0-beta.10.tgz#ba23287043da8172f4f9e53babb620f54ad36189" + integrity sha512-mzMOAqgM95FAgzcVzCH/Q0NfN0CTMHVDWCCFyg4B5ZcsuRiQKqQQw0HS+5uOQDtoZEDl2BqGFby7pGpENWGjZQ== -xterm-headless@4.14.0-beta.8: - version "4.14.0-beta.8" - resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-4.14.0-beta.8.tgz#5591d438cb4940f8cef9a65a4cfc385ad09b9aa5" - integrity sha512-H/vZiuH7cwgGcbxIHCexFVHfCzFX8z0/yaxuDGK/G8G1DoqI3MVNFQPdugu/lkptyddD0O5Hn0dUV7hSCfxi+A== +xterm-headless@4.14.0-beta.11: + version "4.14.0-beta.11" + resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-4.14.0-beta.11.tgz#c97052e31ab07a50c577cdcf05878e4cff76deec" + integrity sha512-EL3cK0yXvQ9BDYqcAMXGd2NkHFFknYQZ7sWgVq6xWrMcSrOMGfIpNyZ1zlP4V5pUk0+yur52TS4xumJ+fYld5w== -xterm@4.14.0-beta.18: - version "4.14.0-beta.18" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.14.0-beta.18.tgz#57a7cbf5544b6e42c7a37d016b1adf37e679e9a3" - integrity sha512-Vyr3G6C+vPEn1MLfDEkZfOhwmG5byiVM1eQ0rf+yLg8a1rz3Oa4ji4IkA6SGr3ztqQ5ZQA6Q/10Rop+1dk7PrA== +xterm@4.14.0-beta.21: + version "4.14.0-beta.21" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.14.0-beta.21.tgz#2d50328389cc79021c0202405689955fc18cb703" + integrity sha512-9ELD78FTUL91OBRfNVWh+gxEqufNNWsrFkkOFxhKBSk3YRuJdcapZBb6afobgpAaQglw8v8Ze1eBkTtctW20jQ== y18n@^3.2.1: version "3.2.2"