diff --git a/code/extensions/emmet/src/test/cssAbbreviationAction.test.ts b/code/extensions/emmet/src/test/cssAbbreviationAction.test.ts index e5fc4b72f5f..42871e92804 100644 --- a/code/extensions/emmet/src/test/cssAbbreviationAction.test.ts +++ b/code/extensions/emmet/src/test/cssAbbreviationAction.test.ts @@ -183,7 +183,9 @@ nav# assert.strictEqual((emmetCompletionItem.documentation || '').replace(/\|/g, ''), expandedText, `Docs of completion item doesnt match.`); }; - return Promise.all([completionPromise1, completionPromise2]).then(([result1, result2]) => { + return Promise.all([completionPromise1, completionPromise2]).then(([result1, result2]) => { + assert.ok(result1); + assert.ok(result2); callBack(result1, '#121212'); callBack(result2, '!important'); editor.selections = [new Selection(2, 12, 2, 12), new Selection(2, 14, 2, 14)]; @@ -244,7 +246,9 @@ nav# assert.strictEqual((emmetCompletionItem.documentation || '').replace(/\|/g, ''), expandedText, `Docs of completion item doesnt match.`); }; - return Promise.all([completionPromise1, completionPromise2]).then(([result1, result2]) => { + return Promise.all([completionPromise1, completionPromise2]).then(([result1, result2]) => { + assert.ok(result1); + assert.ok(result2); callBack(result1, '#121212'); callBack(result2, '!important'); editor.selections = [new Selection(3, 12, 3, 12), new Selection(3, 14, 3, 14)]; @@ -303,7 +307,9 @@ nav# assert.strictEqual((emmetCompletionItem.documentation || '').replace(/\|/g, ''), expandedText, `Docs of completion item doesnt match.`); }; - return Promise.all([completionPromise1, completionPromise2]).then(([result1, result2]) => { + return Promise.all([completionPromise1, completionPromise2]).then(([result1, result2]) => { + assert.ok(result1); + assert.ok(result2); callBack(result1, '#121212'); callBack(result2, '!important'); editor.selections = [new Selection(2, 12, 2, 12), new Selection(2, 14, 2, 14)]; @@ -361,7 +367,9 @@ nav# assert.strictEqual(emmetCompletionItem.filterText, abbreviation, `FilterText of completion item doesnt match.`); }; - return Promise.all([completionPromise1, completionPromise2]).then(([result1, result2]) => { + return Promise.all([completionPromise1, completionPromise2]).then(([result1, result2]) => { + assert.ok(result1); + assert.ok(result2); callBack(result1); callBack(result2); return Promise.resolve(); @@ -421,7 +429,11 @@ nav# assert.strictEqual(emmetCompletionItem.filterText, abbreviation, `FilterText of completion item doesnt match.`); }; - return Promise.all([completionPromise1, completionPromise2, completionPromise3, completionPromise4]).then(([result1, result2, result3, result4]) => { + return Promise.all([completionPromise1, completionPromise2, completionPromise3, completionPromise4]).then(([result1, result2, result3, result4]) => { + assert.ok(result1); + assert.ok(result2); + assert.ok(result3); + assert.ok(result4); callBack(result1, 'p10', 'padding: 10px;'); callBack(result2, 'p20', 'padding: 20px;'); callBack(result3, 'p30', 'padding: 30px;'); diff --git a/code/extensions/microsoft-authentication/src/betterSecretStorage.ts b/code/extensions/microsoft-authentication/src/betterSecretStorage.ts index 14c885a7022..3cc854064b5 100644 --- a/code/extensions/microsoft-authentication/src/betterSecretStorage.ts +++ b/code/extensions/microsoft-authentication/src/betterSecretStorage.ts @@ -240,11 +240,8 @@ export class BetterTokenStorage { }, err => { Logger.error(err); - resolve(tokens); - }).then(resolve, err => { - Logger.error(err); - resolve(tokens); - }); + return tokens; + }).then(resolve); }); this._operationInProgress = false; } diff --git a/code/extensions/typescript-language-features/package.json b/code/extensions/typescript-language-features/package.json index eda688b9a93..6daaef5080b 100644 --- a/code/extensions/typescript-language-features/package.json +++ b/code/extensions/typescript-language-features/package.json @@ -1275,6 +1275,12 @@ "default": false, "description": "%configuration.preferGoToSourceDefinition%", "scope": "window" + }, + "typescript.workspaceSymbols.excludeLibrarySymbols": { + "type": "boolean", + "default": true, + "markdownDescription": "%typescript.workspaceSymbols.excludeLibrarySymbols%", + "scope": "window" } } }, diff --git a/code/extensions/typescript-language-features/package.nls.json b/code/extensions/typescript-language-features/package.nls.json index c235219fef2..dc902befdb2 100644 --- a/code/extensions/typescript-language-features/package.nls.json +++ b/code/extensions/typescript-language-features/package.nls.json @@ -153,6 +153,7 @@ "typescript.preferences.includePackageJsonAutoImports.on": "Always search dependencies.", "typescript.preferences.includePackageJsonAutoImports.off": "Never search dependencies.", "typescript.preferences.autoImportFileExcludePatterns": "Specify glob patterns of files to exclude from auto imports. Relative paths are resolved relative to the workspace root. Patterns are evaluated using tsconfig.json [`exclude`](https://www.typescriptlang.org/tsconfig#exclude) semantics. Requires using TypeScript 4.8 or newer in the workspace.", + "typescript.workspaceSymbols.excludeLibrarySymbols": "Exclude symbols that come from library files in `Go To Symbol in Workspace` results. Requires using TypeScript 5.3+ in the workspace.", "typescript.updateImportsOnFileMove.enabled": "Enable/disable automatic updating of import paths when you rename or move a file in VS Code.", "typescript.updateImportsOnFileMove.enabled.prompt": "Prompt on each rename.", "typescript.updateImportsOnFileMove.enabled.always": "Always update paths automatically.", diff --git a/code/extensions/typescript-language-features/src/configuration/configuration.ts b/code/extensions/typescript-language-features/src/configuration/configuration.ts index 0d60cd74932..5fce20d1d1f 100644 --- a/code/extensions/typescript-language-features/src/configuration/configuration.ts +++ b/code/extensions/typescript-language-features/src/configuration/configuration.ts @@ -122,6 +122,7 @@ export interface TypeScriptServiceConfiguration { readonly enableTsServerTracing: boolean; readonly localNodePath: string | null; readonly globalNodePath: string | null; + readonly workspaceSymbolsExcludeLibrarySymbols: boolean; } export function areServiceConfigurationsEqual(a: TypeScriptServiceConfiguration, b: TypeScriptServiceConfiguration): boolean { @@ -158,6 +159,7 @@ export abstract class BaseServiceConfigurationProvider implements ServiceConfigu enableTsServerTracing: this.readEnableTsServerTracing(configuration), localNodePath: this.readLocalNodePath(configuration), globalNodePath: this.readGlobalNodePath(configuration), + workspaceSymbolsExcludeLibrarySymbols: this.readWorkspaceSymbolsExcludeLibrarySymbols(configuration), }; } @@ -255,4 +257,8 @@ export abstract class BaseServiceConfigurationProvider implements ServiceConfigu private readWebProjectWideIntellisenseSuppressSemanticErrors(configuration: vscode.WorkspaceConfiguration): boolean { return configuration.get('typescript.tsserver.web.projectWideIntellisense.suppressSemanticErrors', true); } + + private readWorkspaceSymbolsExcludeLibrarySymbols(configuration: vscode.WorkspaceConfiguration): boolean { + return configuration.get('typescript.workspaceSymbols.excludeLibrarySymbols', true); + } } diff --git a/code/extensions/typescript-language-features/src/languageFeatures/updatePathsOnRename.ts b/code/extensions/typescript-language-features/src/languageFeatures/updatePathsOnRename.ts index 0d4da5b39a5..91ca9612e70 100644 --- a/code/extensions/typescript-language-features/src/languageFeatures/updatePathsOnRename.ts +++ b/code/extensions/typescript-language-features/src/languageFeatures/updatePathsOnRename.ts @@ -56,38 +56,39 @@ class UpdateImportsOnFileRenameHandler extends Disposable { super(); this._register(vscode.workspace.onDidRenameFiles(async (e) => { - const [{ newUri, oldUri }] = e.files; - const newFilePath = this.client.toTsFilePath(newUri); - if (!newFilePath) { - return; - } + for (const { newUri, oldUri } of e.files) { + const newFilePath = this.client.toTsFilePath(newUri); + if (!newFilePath) { + continue; + } - const oldFilePath = this.client.toTsFilePath(oldUri); - if (!oldFilePath) { - return; - } + const oldFilePath = this.client.toTsFilePath(oldUri); + if (!oldFilePath) { + continue; + } - const config = this.getConfiguration(newUri); - const setting = config.get(updateImportsOnFileMoveName); - if (setting === UpdateImportsOnFileMoveSetting.Never) { - return; - } + const config = this.getConfiguration(newUri); + const setting = config.get(updateImportsOnFileMoveName); + if (setting === UpdateImportsOnFileMoveSetting.Never) { + continue; + } - // Try to get a js/ts file that is being moved - // For directory moves, this returns a js/ts file under the directory. - const jsTsFileThatIsBeingMoved = await this.getJsTsFileBeingMoved(newUri); - if (!jsTsFileThatIsBeingMoved || !this.client.toTsFilePath(jsTsFileThatIsBeingMoved)) { - return; - } + // Try to get a js/ts file that is being moved + // For directory moves, this returns a js/ts file under the directory. + const jsTsFileThatIsBeingMoved = await this.getJsTsFileBeingMoved(newUri); + if (!jsTsFileThatIsBeingMoved || !this.client.toTsFilePath(jsTsFileThatIsBeingMoved)) { + continue; + } - this._pendingRenames.add({ oldUri, newUri, newFilePath, oldFilePath, jsTsFileThatIsBeingMoved }); + this._pendingRenames.add({ oldUri, newUri, newFilePath, oldFilePath, jsTsFileThatIsBeingMoved }); - this._delayer.trigger(() => { - vscode.window.withProgress({ - location: vscode.ProgressLocation.Window, - title: vscode.l10n.t("Checking for update of JS/TS imports") - }, () => this.flushRenames()); - }); + this._delayer.trigger(() => { + vscode.window.withProgress({ + location: vscode.ProgressLocation.Window, + title: vscode.l10n.t("Checking for update of JS/TS imports") + }, () => this.flushRenames()); + }); + } })); } diff --git a/code/extensions/typescript-language-features/src/typescriptServiceClient.ts b/code/extensions/typescript-language-features/src/typescriptServiceClient.ts index 5b7591bfd8f..7553be7ed49 100644 --- a/code/extensions/typescript-language-features/src/typescriptServiceClient.ts +++ b/code/extensions/typescript-language-features/src/typescriptServiceClient.ts @@ -558,6 +558,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType providePrefixAndSuffixTextForRename: true, allowRenameOfImportPath: true, includePackageJsonAutoImports: this._configuration.includePackageJsonAutoImports, + excludeLibrarySymbolsInNavTo: this._configuration.workspaceSymbolsExcludeLibrarySymbols, }, watchOptions }; diff --git a/code/src/typings/thenable.d.ts b/code/src/typings/thenable.d.ts index f4d8608929d..73373eadbae 100644 --- a/code/src/typings/thenable.d.ts +++ b/code/src/typings/thenable.d.ts @@ -9,13 +9,4 @@ * enables reusing existing code without migrating to a specific promise implementation. Still, * we recommend the use of native promises which are available in VS Code. */ -interface Thenable { - /** - * Attaches callbacks for the resolution and/or rejection of the Promise. - * @param onfulfilled The callback to execute when the Promise is resolved. - * @param onrejected The callback to execute when the Promise is rejected. - * @returns A Promise for the completion of which ever callback is executed. - */ - then(onfulfilled?: (value: T) => TResult | Thenable, onrejected?: (reason: any) => TResult | Thenable): Thenable; - then(onfulfilled?: (value: T) => TResult | Thenable, onrejected?: (reason: any) => void): Thenable; -} +interface Thenable extends PromiseLike { } diff --git a/code/src/vs/editor/common/services/markerDecorationsService.ts b/code/src/vs/editor/common/services/markerDecorationsService.ts index c6397b71e0a..de2c54e9e01 100644 --- a/code/src/vs/editor/common/services/markerDecorationsService.ts +++ b/code/src/vs/editor/common/services/markerDecorationsService.ts @@ -16,7 +16,7 @@ import { Range } from 'vs/editor/common/core/range'; import { IMarkerDecorationsService } from 'vs/editor/common/services/markerDecorations'; import { Schemas } from 'vs/base/common/network'; import { Emitter, Event } from 'vs/base/common/event'; -import { minimapWarning, minimapError } from 'vs/platform/theme/common/colorRegistry'; +import { minimapInfo, minimapWarning, minimapError } from 'vs/platform/theme/common/colorRegistry'; import { BidirectionalMap, ResourceMap } from 'vs/base/common/map'; import { diffSets } from 'vs/base/common/collections'; @@ -208,6 +208,15 @@ class MarkerDecorations extends Disposable { } zIndex = 0; break; + case MarkerSeverity.Info: + className = ClassName.EditorInfoDecoration; + color = themeColorFromId(overviewRulerInfo); + zIndex = 10; + minimap = { + color: themeColorFromId(minimapInfo), + position: MinimapPosition.Inline + }; + break; case MarkerSeverity.Warning: className = ClassName.EditorWarningDecoration; color = themeColorFromId(overviewRulerWarning); @@ -217,11 +226,6 @@ class MarkerDecorations extends Disposable { position: MinimapPosition.Inline }; break; - case MarkerSeverity.Info: - className = ClassName.EditorInfoDecoration; - color = themeColorFromId(overviewRulerInfo); - zIndex = 10; - break; case MarkerSeverity.Error: default: className = ClassName.EditorErrorDecoration; diff --git a/code/src/vs/editor/contrib/codelens/browser/codelensController.ts b/code/src/vs/editor/contrib/codelens/browser/codelensController.ts index da810c9a099..f260a892cca 100644 --- a/code/src/vs/editor/contrib/codelens/browser/codelensController.ts +++ b/code/src/vs/editor/contrib/codelens/browser/codelensController.ts @@ -134,7 +134,7 @@ export class CodeLensContribution implements IEditorContribution { return; } - if (!this._editor.getOption(EditorOption.codeLens)) { + if (!this._editor.getOption(EditorOption.codeLens) || model.isTooLargeForTokenization()) { return; } diff --git a/code/src/vs/editor/contrib/links/browser/links.ts b/code/src/vs/editor/contrib/links/browser/links.ts index 3786b9daa87..9527453bf32 100644 --- a/code/src/vs/editor/contrib/links/browser/links.ts +++ b/code/src/vs/editor/contrib/links/browser/links.ts @@ -119,6 +119,10 @@ export class LinkDetector extends Disposable implements IEditorContribution { const model = this.editor.getModel(); + if (model.isTooLargeForSyncing()) { + return; + } + if (!this.providers.has(model)) { return; } diff --git a/code/src/vs/editor/contrib/stickyScroll/browser/stickyScrollProvider.ts b/code/src/vs/editor/contrib/stickyScroll/browser/stickyScrollProvider.ts index 3269b037cd6..3388380f97c 100644 --- a/code/src/vs/editor/contrib/stickyScroll/browser/stickyScrollProvider.ts +++ b/code/src/vs/editor/contrib/stickyScroll/browser/stickyScrollProvider.ts @@ -112,7 +112,7 @@ export class StickyLineCandidateProvider extends Disposable implements IStickyLi private async updateStickyModel(token: CancellationToken): Promise { - if (!this._editor.hasModel() || !this._stickyModelProvider) { + if (!this._editor.hasModel() || !this._stickyModelProvider || this._editor.getModel().isTooLargeForTokenization()) { this._model = null; return; } diff --git a/code/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts b/code/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts index a11868bc8cb..943bad34e80 100644 --- a/code/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts +++ b/code/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts @@ -490,7 +490,7 @@ export class WordHighlighterContribution extends Disposable implements IEditorCo this.wordHighlighter = null; this.linkedContributions = new Set(); const createWordHighlighterIfPossible = () => { - if (editor.hasModel()) { + if (editor.hasModel() && !editor.getModel().isTooLargeForTokenization()) { this.wordHighlighter = new WordHighlighter(editor, languageFeaturesService.documentHighlightProvider, () => Iterable.map(this.linkedContributions, c => c.wordHighlighter), contextKeyService); } }; diff --git a/code/src/vs/platform/theme/common/colorRegistry.ts b/code/src/vs/platform/theme/common/colorRegistry.ts index adc559c304e..9f66d2cd1a5 100644 --- a/code/src/vs/platform/theme/common/colorRegistry.ts +++ b/code/src/vs/platform/theme/common/colorRegistry.ts @@ -395,7 +395,7 @@ export const editorInlayHintParameterForeground = registerColor('editorInlayHint export const editorInlayHintParameterBackground = registerColor('editorInlayHint.parameterBackground', { dark: editorInlayHintBackground, light: editorInlayHintBackground, hcDark: editorInlayHintBackground, hcLight: editorInlayHintBackground }, nls.localize('editorInlayHintBackgroundParameter', 'Background color of inline hints for parameters')); /** - * Editor lighbulb icon colors + * Editor lightbulb icon colors */ export const editorLightBulbForeground = registerColor('editorLightBulb.foreground', { dark: '#FFCC00', light: '#DDB100', hcDark: '#FFCC00', hcLight: '#007ACC' }, nls.localize('editorLightBulbForeground', "The color used for the lightbulb actions icon.")); export const editorLightBulbAutoFixForeground = registerColor('editorLightBulbAutoFix.foreground', { dark: '#75BEFF', light: '#007ACC', hcDark: '#75BEFF', hcLight: '#007ACC' }, nls.localize('editorLightBulbAutoFixForeground', "The color used for the lightbulb auto fix actions icon.")); @@ -547,8 +547,9 @@ export const overviewRulerSelectionHighlightForeground = registerColor('editorOv export const minimapFindMatch = registerColor('minimap.findMatchHighlight', { light: '#d18616', dark: '#d18616', hcDark: '#AB5A00', hcLight: '#0F4A85' }, nls.localize('minimapFindMatchHighlight', 'Minimap marker color for find matches.'), true); export const minimapSelectionOccurrenceHighlight = registerColor('minimap.selectionOccurrenceHighlight', { light: '#c9c9c9', dark: '#676767', hcDark: '#ffffff', hcLight: '#0F4A85' }, nls.localize('minimapSelectionOccurrenceHighlight', 'Minimap marker color for repeating editor selections.'), true); export const minimapSelection = registerColor('minimap.selectionHighlight', { light: '#ADD6FF', dark: '#264F78', hcDark: '#ffffff', hcLight: '#0F4A85' }, nls.localize('minimapSelectionHighlight', 'Minimap marker color for the editor selection.'), true); -export const minimapError = registerColor('minimap.errorHighlight', { dark: new Color(new RGBA(255, 18, 18, 0.7)), light: new Color(new RGBA(255, 18, 18, 0.7)), hcDark: new Color(new RGBA(255, 50, 50, 1)), hcLight: '#B5200D' }, nls.localize('minimapError', 'Minimap marker color for errors.')); +export const minimapInfo = registerColor('minimap.infoHighlight', { dark: editorInfoForeground, light: editorInfoForeground, hcDark: editorInfoBorder, hcLight: editorInfoBorder }, nls.localize('minimapInfo', 'Minimap marker color for infos.')); export const minimapWarning = registerColor('minimap.warningHighlight', { dark: editorWarningForeground, light: editorWarningForeground, hcDark: editorWarningBorder, hcLight: editorWarningBorder }, nls.localize('overviewRuleWarning', 'Minimap marker color for warnings.')); +export const minimapError = registerColor('minimap.errorHighlight', { dark: new Color(new RGBA(255, 18, 18, 0.7)), light: new Color(new RGBA(255, 18, 18, 0.7)), hcDark: new Color(new RGBA(255, 50, 50, 1)), hcLight: '#B5200D' }, nls.localize('minimapError', 'Minimap marker color for errors.')); export const minimapBackground = registerColor('minimap.background', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('minimapBackground', "Minimap background color.")); export const minimapForegroundOpacity = registerColor('minimap.foregroundOpacity', { dark: Color.fromHex('#000f'), light: Color.fromHex('#000f'), hcDark: Color.fromHex('#000f'), hcLight: Color.fromHex('#000f') }, nls.localize('minimapForegroundOpacity', 'Opacity of foreground elements rendered in the minimap. For example, "#000000c0" will render the elements with 75% opacity.')); diff --git a/code/src/vs/workbench/api/common/extHostExtensionService.ts b/code/src/vs/workbench/api/common/extHostExtensionService.ts index f0f60cf6f7d..99fccebaee0 100644 --- a/code/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/code/src/vs/workbench/api/common/extHostExtensionService.ts @@ -740,8 +740,18 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme return new Promise((resolve, reject) => { const oldTestRunnerCallback = (error: Error, failures: number | undefined) => { if (error) { + if (isCI) { + this._logService.error(`Test runner called back with error`, error); + } reject(error); } else { + if (isCI) { + if (failures) { + this._logService.info(`Test runner called back with ${failures} failures.`); + } else { + this._logService.info(`Test runner called back with successful outcome.`); + } + } resolve((typeof failures === 'number' && failures > 0) ? 1 /* ERROR */ : 0 /* OK */); } }; @@ -754,9 +764,15 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme if (runResult && runResult.then) { runResult .then(() => { + if (isCI) { + this._logService.info(`Test runner finished successfully.`); + } resolve(0); }) .catch((err: unknown) => { + if (isCI) { + this._logService.error(`Test runner finished with error`, err); + } reject(err instanceof Error && err.stack ? err.stack : String(err)); }); } diff --git a/code/src/vs/workbench/contrib/codeEditor/browser/largeFileOptimizations.ts b/code/src/vs/workbench/contrib/codeEditor/browser/largeFileOptimizations.ts index 52ac3a4afb1..9c2cae83890 100644 --- a/code/src/vs/workbench/contrib/codeEditor/browser/largeFileOptimizations.ts +++ b/code/src/vs/workbench/contrib/codeEditor/browser/largeFileOptimizations.ts @@ -44,7 +44,7 @@ export class LargeFileOptimizationsWarner extends Disposable implements IEditorC 'Variable 0 will be a file name.' ] }, - "{0}: tokenization, wrapping, folding and sticky scroll have been turned off for this large file in order to reduce memory usage and avoid freezing or crashing.", + "{0}: tokenization, wrapping, folding, codelens, word highlighting and sticky scroll have been turned off for this large file in order to reduce memory usage and avoid freezing or crashing.", path.basename(model.uri.path) ); diff --git a/code/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts b/code/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts index afd23e228d5..3abdc920606 100644 --- a/code/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts +++ b/code/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts @@ -81,8 +81,8 @@ class ToggleBreakpointAction extends Action2 { // Does not account for multi line selections, Set to remove multiple cursor on the same line const lineNumbers = [...new Set(editor.getSelections().map(s => s.getPosition().lineNumber))]; - const bps = debugService.getModel().getBreakpoints(); await Promise.all(lineNumbers.map(async line => { + const bps = debugService.getModel().getBreakpoints({ lineNumber: line, uri: modelUri }); if (bps.length) { await Promise.all(bps.map(bp => debugService.removeBreakpoints(bp.getId()))); } else if (canSet) { diff --git a/code/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts b/code/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts index 9495543aad6..886278071b0 100644 --- a/code/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts +++ b/code/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts @@ -448,6 +448,9 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo async run(accessor: ServicesAccessor, editSessionId?: string): Promise { const data = await that.quickInputService.input({ prompt: 'Enter serialized data' }); + if (data) { + that.editSessionsStorageService.lastReadResources.set('editSessions', { content: data, ref: '' }); + } await that.progressService.withProgress({ ...resumeProgressOptions, title: resumeProgressOptionsTitle }, async () => await that.resumeEditSession(editSessionId, undefined, undefined, undefined, undefined, data)); } })); diff --git a/code/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts b/code/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts index cf6589b6027..96903926179 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts @@ -28,12 +28,13 @@ import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } fr import { IEditorPane } from 'vs/workbench/common/editor'; import { CellRevealType, INotebookEditorOptions, INotebookEditorPane } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookEditor'; -import { NotebookCellOutlineProvider, OutlineEntry } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProvider'; -import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NotebookCellOutlineProvider } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProvider'; +import { CellKind, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IOutline, IOutlineComparator, IOutlineCreator, IOutlineListConfig, IOutlineService, IQuickPickDataSource, IQuickPickOutlineElement, OutlineChangeEvent, OutlineConfigCollapseItemsValues, OutlineConfigKeys, OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; - +import { OutlineEntry } from 'vs/workbench/contrib/notebook/browser/viewModel/OutlineEntry'; +import { CancellationToken } from 'vs/base/common/cancellation'; class NotebookOutlineTemplate { @@ -156,14 +157,16 @@ class NotebookQuickPickProvider implements IQuickPickDataSource { } const result: IQuickPickOutlineElement[] = []; const { hasFileIcons } = this._themeService.getFileIconTheme(); + for (const element of bucket) { + const useFileIcon = hasFileIcons && !element.symbolKind; // todo@jrieken it is fishy that codicons cannot be used with iconClasses // but file icons can... result.push({ element, - label: hasFileIcons ? element.label : `$(${element.icon.id}) ${element.label}`, + label: useFileIcon ? element.label : `$(${element.icon.id}) ${element.label}`, ariaLabel: element.label, - iconClasses: hasFileIcons ? getIconClassesForLanguageId(element.cell.language ?? '') : undefined, + iconClasses: useFileIcon ? getIconClassesForLanguageId(element.cell.language ?? '') : undefined, }); } return result; @@ -273,6 +276,10 @@ export class NotebookCellOutline implements IOutline { }; } + async setFullSymbols(cancelToken: CancellationToken) { + await this._outlineProvider?.setFullSymbols(cancelToken); + } + get uri(): URI | undefined { return this._outlineProvider?.uri; } @@ -285,7 +292,8 @@ export class NotebookCellOutline implements IOutline { options: { ...options, override: this._editor.input?.editorId, - cellRevealType: CellRevealType.NearTopIfOutsideViewport + cellRevealType: CellRevealType.NearTopIfOutsideViewport, + selection: entry.position } as INotebookEditorOptions, }, sideBySide ? SIDE_GROUP : undefined); } @@ -330,6 +338,7 @@ export class NotebookOutlineCreator implements IOutlineCreator reg.dispose(); @@ -339,8 +348,14 @@ export class NotebookOutlineCreator implements IOutlineCreator | undefined> { - return this._instantiationService.createInstance(NotebookCellOutline, editor, target); + async createOutline(editor: NotebookEditor, target: OutlineTarget, cancelToken: CancellationToken): Promise | undefined> { + const outline = this._instantiationService.createInstance(NotebookCellOutline, editor, target); + + const showAllSymbols = this._configurationService.getValue(NotebookSetting.gotoSymbolsAllSymbols); + if (target === OutlineTarget.QuickPick && showAllSymbols) { + await outline.setFullSymbols(cancelToken); + } + return outline; } } @@ -362,5 +377,10 @@ Registry.as(ConfigurationExtensions.Configuration).regis default: true, markdownDescription: localize('breadcrumbs.showCodeCells', "When enabled notebook breadcrumbs contain code cells.") }, + [NotebookSetting.gotoSymbolsAllSymbols]: { + type: 'boolean', + default: false, + markdownDescription: localize('notebook.gotoSymbols.showAllSymbols', "When enabled goto symbol quickpick will display full code symbols from the notebook, as well as markdown headers.") + }, } }); diff --git a/code/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/code/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index a02c7062c59..6372ee6dada 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -119,7 +119,6 @@ import { runAccessibilityHelpAction, showAccessibleOutput } from 'vs/workbench/c import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { AccessibilityHelpAction, AccessibleViewAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; - /*--------------------------------------------------------------------------------------------- */ Registry.as(EditorExtensions.EditorPane).registerEditorPane( diff --git a/code/src/vs/workbench/contrib/notebook/browser/viewModel/OutlineEntry.ts b/code/src/vs/workbench/contrib/notebook/browser/viewModel/OutlineEntry.ts new file mode 100644 index 00000000000..9fd1c4f7160 --- /dev/null +++ b/code/src/vs/workbench/contrib/notebook/browser/viewModel/OutlineEntry.ts @@ -0,0 +1,113 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { Codicon } from 'vs/base/common/codicons'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { IMarkerService, MarkerSeverity } from 'vs/platform/markers/common/markers'; +import { ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { executingStateIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; +import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { Range } from 'vs/editor/common/core/range'; +import { SymbolKind, SymbolKinds } from 'vs/editor/common/languages'; + + +export interface IOutlineMarkerInfo { + readonly count: number; + readonly topSev: MarkerSeverity; +} + +export class OutlineEntry { + private _children: OutlineEntry[] = []; + private _parent: OutlineEntry | undefined; + private _markerInfo: IOutlineMarkerInfo | undefined; + + get icon(): ThemeIcon { + if (this.symbolKind) { + return SymbolKinds.toIcon(this.symbolKind); + } + return this.isExecuting && this.isPaused ? executingStateIcon : + this.isExecuting ? ThemeIcon.modify(executingStateIcon, 'spin') : + this.cell.cellKind === CellKind.Markup ? Codicon.markdown : Codicon.code; + } + + constructor( + readonly index: number, + readonly level: number, + readonly cell: ICellViewModel, + readonly label: string, + readonly isExecuting: boolean, + readonly isPaused: boolean, + readonly position?: Range, + readonly symbolKind?: SymbolKind, + ) { } + + addChild(entry: OutlineEntry) { + this._children.push(entry); + entry._parent = this; + } + + get parent(): OutlineEntry | undefined { + return this._parent; + } + + get children(): Iterable { + return this._children; + } + + get markerInfo(): IOutlineMarkerInfo | undefined { + return this._markerInfo; + } + + updateMarkers(markerService: IMarkerService): void { + if (this.cell.cellKind === CellKind.Code) { + // a code cell can have marker + const marker = markerService.read({ resource: this.cell.uri, severities: MarkerSeverity.Error | MarkerSeverity.Warning }); + if (marker.length === 0) { + this._markerInfo = undefined; + } else { + const topSev = marker.find(a => a.severity === MarkerSeverity.Error)?.severity ?? MarkerSeverity.Warning; + this._markerInfo = { topSev, count: marker.length }; + } + } else { + // a markdown cell can inherit markers from its children + let topChild: MarkerSeverity | undefined; + for (const child of this.children) { + child.updateMarkers(markerService); + if (child.markerInfo) { + topChild = !topChild ? child.markerInfo.topSev : Math.max(child.markerInfo.topSev, topChild); + } + } + this._markerInfo = topChild && { topSev: topChild, count: 0 }; + } + } + + clearMarkers(): void { + this._markerInfo = undefined; + for (const child of this.children) { + child.clearMarkers(); + } + } + + find(cell: ICellViewModel, parents: OutlineEntry[]): OutlineEntry | undefined { + if (cell.id === this.cell.id) { + return this; + } + parents.push(this); + for (const child of this.children) { + const result = child.find(cell, parents); + if (result) { + return result; + } + } + parents.pop(); + return undefined; + } + + asFlatList(bucket: OutlineEntry[]): void { + bucket.push(this); + for (const child of this.children) { + child.asFlatList(bucket); + } + } +} diff --git a/code/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineEntryFactory.ts b/code/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineEntryFactory.ts new file mode 100644 index 00000000000..0671f846db6 --- /dev/null +++ b/code/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineEntryFactory.ts @@ -0,0 +1,133 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { renderMarkdownAsPlaintext } from 'vs/base/browser/markdownRenderer'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { IOutlineModelService, OutlineModelService } from 'vs/editor/contrib/documentSymbols/browser/outlineModel'; +import { localize } from 'vs/nls'; +import { ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { getMarkdownHeadersInCell } from 'vs/workbench/contrib/notebook/browser/viewModel/foldingModel'; +import { OutlineEntry } from './OutlineEntry'; +import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; +import { Range } from 'vs/editor/common/core/range'; +import { ITextModel } from 'vs/editor/common/model'; +import { SymbolKind } from 'vs/editor/common/languages'; + +type entryDesc = { + name: string; + position: Range; + level: number; + kind: SymbolKind; +}; + +export class NotebookOutlineEntryFactory { + + private cellOutlineEntryCache: Record = {}; + + constructor( + private readonly executionStateService: INotebookExecutionStateService + ) { } + + public getOutlineEntries(cell: ICellViewModel, index: number): OutlineEntry[] { + const entries: OutlineEntry[] = []; + + const isMarkdown = cell.cellKind === CellKind.Markup; + + // cap the amount of characters that we look at and use the following logic + // - for MD prefer headings (each header is an entry) + // - otherwise use the first none-empty line of the cell (MD or code) + let content = getCellFirstNonEmptyLine(cell); + let hasHeader = false; + + if (isMarkdown) { + const fullContent = cell.getText().substring(0, 10000); + for (const { depth, text } of getMarkdownHeadersInCell(fullContent)) { + hasHeader = true; + entries.push(new OutlineEntry(index++, depth, cell, text, false, false)); + } + + if (!hasHeader) { + // no markdown syntax headers, try to find html tags + const match = fullContent.match(/(.*)<\/h\1>/i); + if (match) { + hasHeader = true; + const level = parseInt(match[1]); + const text = match[2].trim(); + entries.push(new OutlineEntry(index++, level, cell, text, false, false)); + } + } + + if (!hasHeader) { + content = renderMarkdownAsPlaintext({ value: content }); + } + } + + if (!hasHeader) { + if (!isMarkdown && cell.model.textModel) { + const cachedEntries = this.cellOutlineEntryCache[cell.model.textModel.id]; + + // Gathering symbols from the model is an async operation, but this provider is syncronous. + // So symbols need to be precached before this function is called to get the full list. + if (cachedEntries) { + cachedEntries.forEach((cached) => { + entries.push(new OutlineEntry(index++, cached.level, cell, cached.name, false, false, cached.position, cached.kind)); + }); + + } + } + + const exeState = !isMarkdown && this.executionStateService.getCellExecution(cell.uri); + if (entries.length === 0) { + let preview = content.trim(); + if (preview.length === 0) { + // empty or just whitespace + preview = localize('empty', "empty cell"); + } + + entries.push(new OutlineEntry(index++, 7, cell, preview, !!exeState, exeState ? exeState.isPaused : false)); + } + } + + return entries; + } + + public async cacheSymbols(textModel: ITextModel, outlineModelService: IOutlineModelService, cancelToken: CancellationToken) { + const outlineModel = await outlineModelService.getOrCreate(textModel, cancelToken); + const entries = createOutlineEntries(outlineModel.getTopLevelSymbols(), 7); + this.cellOutlineEntryCache[textModel.id] = entries; + } +} + +type outlineModel = Awaited>; +type documentSymbol = ReturnType[number]; + +function createOutlineEntries(symbols: documentSymbol[], level: number): entryDesc[] { + const entries: entryDesc[] = []; + symbols.forEach(symbol => { + const position = new Range(symbol.selectionRange.startLineNumber, + symbol.selectionRange.startColumn, + symbol.selectionRange.startLineNumber, + symbol.selectionRange.startColumn); + entries.push({ name: symbol.name, position, level, kind: symbol.kind }); + if (symbol.children) { + entries.push(...createOutlineEntries(symbol.children, level + 1)); + } + }); + return entries; +} + +function getCellFirstNonEmptyLine(cell: ICellViewModel) { + const textBuffer = cell.textBuffer; + for (let i = 0; i < textBuffer.getLineCount(); i++) { + const firstNonWhitespace = textBuffer.getLineFirstNonWhitespaceColumn(i + 1); + const lineLength = textBuffer.getLineLength(i + 1); + if (firstNonWhitespace < lineLength) { + return textBuffer.getLineContent(i + 1); + } + } + + return cell.getText().substring(0, 100); +} diff --git a/code/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProvider.ts b/code/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProvider.ts index 47971ec803e..7e5f1d22b66 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProvider.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProvider.ts @@ -3,120 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from 'vs/nls'; -import { renderMarkdownAsPlaintext } from 'vs/base/browser/markdownRenderer'; -import { Codicon } from 'vs/base/common/codicons'; import { Emitter, Event } from 'vs/base/common/event'; import { DisposableStore, MutableDisposable, combinedDisposable } from 'vs/base/common/lifecycle'; import { isEqual } from 'vs/base/common/resources'; -import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IMarkerService, MarkerSeverity } from 'vs/platform/markers/common/markers'; +import { IMarkerService } from 'vs/platform/markers/common/markers'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { IActiveNotebookEditor, ICellViewModel, INotebookEditor, INotebookViewCellsUpdateEvent } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { executingStateIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; -import { getMarkdownHeadersInCell } from 'vs/workbench/contrib/notebook/browser/viewModel/foldingModel'; +import { IActiveNotebookEditor, INotebookEditor, INotebookViewCellsUpdateEvent } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookExecutionStateService, NotebookExecutionType } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { OutlineChangeEvent, OutlineConfigKeys, OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; - -export interface IOutlineMarkerInfo { - readonly count: number; - readonly topSev: MarkerSeverity; -} - -export class OutlineEntry { - private _children: OutlineEntry[] = []; - private _parent: OutlineEntry | undefined; - private _markerInfo: IOutlineMarkerInfo | undefined; - - get icon(): ThemeIcon { - return this.isExecuting && this.isPaused ? executingStateIcon : - this.isExecuting ? ThemeIcon.modify(executingStateIcon, 'spin') : - this.cell.cellKind === CellKind.Markup ? Codicon.markdown : Codicon.code; - } - - constructor( - readonly index: number, - readonly level: number, - readonly cell: ICellViewModel, - readonly label: string, - readonly isExecuting: boolean, - readonly isPaused: boolean - ) { } - - addChild(entry: OutlineEntry) { - this._children.push(entry); - entry._parent = this; - } - - get parent(): OutlineEntry | undefined { - return this._parent; - } - - get children(): Iterable { - return this._children; - } - - get markerInfo(): IOutlineMarkerInfo | undefined { - return this._markerInfo; - } - - updateMarkers(markerService: IMarkerService): void { - if (this.cell.cellKind === CellKind.Code) { - // a code cell can have marker - const marker = markerService.read({ resource: this.cell.uri, severities: MarkerSeverity.Error | MarkerSeverity.Warning }); - if (marker.length === 0) { - this._markerInfo = undefined; - } else { - const topSev = marker.find(a => a.severity === MarkerSeverity.Error)?.severity ?? MarkerSeverity.Warning; - this._markerInfo = { topSev, count: marker.length }; - } - } else { - // a markdown cell can inherit markers from its children - let topChild: MarkerSeverity | undefined; - for (const child of this.children) { - child.updateMarkers(markerService); - if (child.markerInfo) { - topChild = !topChild ? child.markerInfo.topSev : Math.max(child.markerInfo.topSev, topChild); - } - } - this._markerInfo = topChild && { topSev: topChild, count: 0 }; - } - } - - clearMarkers(): void { - this._markerInfo = undefined; - for (const child of this.children) { - child.clearMarkers(); - } - } - - find(cell: ICellViewModel, parents: OutlineEntry[]): OutlineEntry | undefined { - if (cell.id === this.cell.id) { - return this; - } - parents.push(this); - for (const child of this.children) { - const result = child.find(cell, parents); - if (result) { - return result; - } - } - parents.pop(); - return undefined; - } - - asFlatList(bucket: OutlineEntry[]): void { - bucket.push(this); - for (const child of this.children) { - child.asFlatList(bucket); - } - } -} - +import { OutlineEntry } from './OutlineEntry'; +import { IOutlineModelService } from 'vs/editor/contrib/documentSymbols/browser/outlineModel'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { NotebookOutlineEntryFactory } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineEntryFactory'; export class NotebookCellOutlineProvider { private readonly _dispoables = new DisposableStore(); @@ -139,15 +40,19 @@ export class NotebookCellOutlineProvider { return this._activeEntry; } + private readonly _outlineEntryFactory: NotebookOutlineEntryFactory; + constructor( private readonly _editor: INotebookEditor, private readonly _target: OutlineTarget, @IThemeService themeService: IThemeService, - @IEditorService _editorService: IEditorService, + @INotebookExecutionStateService notebookExecutionStateService: INotebookExecutionStateService, + @IOutlineModelService private readonly _outlineModelService: IOutlineModelService, @IMarkerService private readonly _markerService: IMarkerService, @IConfigurationService private readonly _configurationService: IConfigurationService, - @INotebookExecutionStateService private readonly _notebookExecutionStateService: INotebookExecutionStateService, ) { + this._outlineEntryFactory = new NotebookOutlineEntryFactory(notebookExecutionStateService); + const selectionListener = new MutableDisposable(); this._dispoables.add(selectionListener); @@ -174,7 +79,7 @@ export class NotebookCellOutlineProvider { this._onDidChange.fire({}); })); - this._dispoables.add(_notebookExecutionStateService.onDidChangeExecution(e => { + this._dispoables.add(notebookExecutionStateService.onDidChangeExecution(e => { if (e.type === NotebookExecutionType.cell && !!this._editor.textModel && e.affectsNotebook(this._editor.textModel?.uri)) { this._recomputeState(); } @@ -194,10 +99,29 @@ export class NotebookCellOutlineProvider { this._recomputeState(); } + async setFullSymbols(cancelToken: CancellationToken) { + const notebookEditorWidget = this._editor; + + const notebookCells = notebookEditorWidget?.getViewModel()?.viewCells.filter((cell) => cell.cellKind === CellKind.Code); + + this._entries.length = 0; + if (notebookCells) { + const promises: Promise[] = []; + for (const cell of notebookCells) { + if (cell.textModel) { + // gather all symbols asynchronously + promises.push(this._outlineEntryFactory.cacheSymbols(cell.textModel, this._outlineModelService, cancelToken)); + } + } + await Promise.allSettled(promises); + } + + this._recomputeState(); + } + private _recomputeState(): void { this._entriesDisposables.clear(); this._activeEntry = undefined; - this._entries.length = 0; this._uri = undefined; if (!this._editor.hasModel()) { @@ -219,61 +143,11 @@ export class NotebookCellOutlineProvider { includeCodeCells = this._configurationService.getValue('notebook.breadcrumbs.showCodeCells'); } - const focusedCellIndex = notebookEditorWidget.getFocus().start; - const focused = notebookEditorWidget.cellAt(focusedCellIndex)?.handle; - const entries: OutlineEntry[] = []; - - for (let i = 0; i < notebookEditorWidget.getLength(); i++) { - const cell = notebookEditorWidget.cellAt(i); - const isMarkdown = cell.cellKind === CellKind.Markup; - if (!isMarkdown && !includeCodeCells) { - continue; - } - - // cap the amount of characters that we look at and use the following logic - // - for MD prefer headings (each header is an entry) - // - otherwise use the first none-empty line of the cell (MD or code) - let content = this._getCellFirstNonEmptyLine(cell); - let hasHeader = false; - - if (isMarkdown) { - const fullContent = cell.getText().substring(0, 10_000); - for (const { depth, text } of getMarkdownHeadersInCell(fullContent)) { - hasHeader = true; - entries.push(new OutlineEntry(entries.length, depth, cell, text, false, false)); - } - - if (!hasHeader) { - // no markdown syntax headers, try to find html tags - const match = fullContent.match(/(.*)<\/h\1>/i); - if (match) { - hasHeader = true; - const level = parseInt(match[1]); - const text = match[2].trim(); - entries.push(new OutlineEntry(entries.length, level, cell, text, false, false)); - } - } - - if (!hasHeader) { - content = renderMarkdownAsPlaintext({ value: content }); - } - } - - if (!hasHeader) { - let preview = content.trim(); - if (preview.length === 0) { - // empty or just whitespace - preview = localize('empty', "empty cell"); - } - - const exeState = !isMarkdown && this._notebookExecutionStateService.getCellExecution(cell.uri); - entries.push(new OutlineEntry(entries.length, 7, cell, preview, !!exeState, exeState ? exeState.isPaused : false)); - } - - if (cell.handle === focused) { - this._activeEntry = entries[entries.length - 1]; - } + const notebookCells = notebookEditorWidget.getViewModel().viewCells.filter((cell) => cell.cellKind === CellKind.Markup || includeCodeCells); + const entries: OutlineEntry[] = []; + for (const cell of notebookCells) { + entries.push(...this._outlineEntryFactory.getOutlineEntries(cell, entries.length)); // send an event whenever any of the cells change this._entriesDisposables.add(cell.model.onDidChangeContent(() => { this._recomputeState(); @@ -355,6 +229,7 @@ export class NotebookCellOutlineProvider { } })); + this._recomputeActive(); this._onDidChange.fire({}); } @@ -381,18 +256,7 @@ export class NotebookCellOutlineProvider { } } - private _getCellFirstNonEmptyLine(cell: ICellViewModel) { - const textBuffer = cell.textBuffer; - for (let i = 0; i < textBuffer.getLineCount(); i++) { - const firstNonWhitespace = textBuffer.getLineFirstNonWhitespaceColumn(i + 1); - const lineLength = textBuffer.getLineLength(i + 1); - if (firstNonWhitespace < lineLength) { - return textBuffer.getLineContent(i + 1); - } - } - return cell.getText().substring(0, 10_000); - } get isEmpty(): boolean { return this._entries.length === 0; diff --git a/code/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts b/code/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts index 426f767e89d..be4036595ac 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts @@ -15,8 +15,10 @@ import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { INotebookCellList } from 'vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon'; -import { NotebookCellOutlineProvider, OutlineEntry } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProvider'; +import { NotebookCellOutlineProvider } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProvider'; + import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { OutlineEntry } from 'vs/workbench/contrib/notebook/browser/viewModel/OutlineEntry'; export class ToggleNotebookStickyScroll extends Action2 { diff --git a/code/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/code/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 343e77b210a..cdd7b0fc31d 100644 --- a/code/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/code/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -962,7 +962,8 @@ export const NotebookSetting = { findScope: 'notebook.find.scope', logging: 'notebook.logging', confirmDeleteRunningCell: 'notebook.confirmDeleteRunningCell', - remoteSaving: 'notebook.experimental.remoteSave' + remoteSaving: 'notebook.experimental.remoteSave', + gotoSymbolsAllSymbols: 'notebook.gotoSymbols.showAllSymbols' } as const; export const enum CellStatusbarAlignment { diff --git a/code/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookSymbols.test.ts b/code/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookSymbols.test.ts new file mode 100644 index 00000000000..79d34e42b58 --- /dev/null +++ b/code/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookSymbols.test.ts @@ -0,0 +1,134 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { mock } from 'vs/base/test/common/mock'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { ITextModel } from 'vs/editor/common/model'; +import { IOutlineModelService, OutlineModel } from 'vs/editor/contrib/documentSymbols/browser/outlineModel'; +import { ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebookOutlineEntryFactory } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineEntryFactory'; +import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; + +suite('Notebook Symbols', function () { + ensureNoDisposablesAreLeakedInTestSuite(); + + type textSymbol = { name: string; selectionRange: {}; children?: textSymbol[] }; + const symbolsPerTextModel: Record = {}; + function setSymbolsForTextModel(symbols: textSymbol[], textmodelId = 'textId') { + symbolsPerTextModel[textmodelId] = symbols; + } + + const executionService = new class extends mock() { + override getCellExecution() { return undefined; } + }; + + class OutlineModelStub { + constructor(private textId: string) { } + + getTopLevelSymbols() { + return symbolsPerTextModel[this.textId]; + } + } + const outlineModelService = new class extends mock() { + override getOrCreate(model: ITextModel, arg1: any) { + const outline = new OutlineModelStub(model.id) as unknown as OutlineModel; + return Promise.resolve(outline); + } + override getDebounceValue(arg0: any) { + return 0; + } + }; + + function createCellViewModel(version: number = 1, textmodelId = 'textId') { + return { + textBuffer: { + getLineCount() { return 0; } + }, + getText() { + return '# code'; + }, + model: { + textModel: { + id: textmodelId, + getVersionId() { return version; } + } + } + } as ICellViewModel; + } + + test('Cell without symbols cache', function () { + setSymbolsForTextModel([{ name: 'var', selectionRange: {} }]); + const entryFactory = new NotebookOutlineEntryFactory(executionService); + const entries = entryFactory.getOutlineEntries(createCellViewModel(), 0); + + assert.equal(entries.length, 1, 'no entries created'); + assert.equal(entries[0].label, '# code', 'entry should fall back to first line of cell'); + }); + + test('Cell with simple symbols', async function () { + setSymbolsForTextModel([{ name: 'var1', selectionRange: {} }, { name: 'var2', selectionRange: {} }]); + const entryFactory = new NotebookOutlineEntryFactory(executionService); + const cell = createCellViewModel(); + + await entryFactory.cacheSymbols(cell.model.textModel!, outlineModelService, CancellationToken.None); + const entries = entryFactory.getOutlineEntries(cell, 0); + + assert.equal(entries.length, 2, 'wrong number of outline entries'); + assert.equal(entries[0].label, 'var1'); + // 6 levels for markdown, all code symbols are greater than the max markdown level + assert.equal(entries[0].level, 7); + assert.equal(entries[0].index, 0); + assert.equal(entries[1].label, 'var2'); + assert.equal(entries[1].level, 7); + assert.equal(entries[1].index, 1); + }); + + test('Cell with nested symbols', async function () { + setSymbolsForTextModel([ + { name: 'root1', selectionRange: {}, children: [{ name: 'nested1', selectionRange: {} }, { name: 'nested2', selectionRange: {} }] }, + { name: 'root2', selectionRange: {}, children: [{ name: 'nested1', selectionRange: {} }] } + ]); + const entryFactory = new NotebookOutlineEntryFactory(executionService); + const cell = createCellViewModel(); + + await entryFactory.cacheSymbols(cell.model.textModel!, outlineModelService, CancellationToken.None); + const entries = entryFactory.getOutlineEntries(createCellViewModel(), 0); + + assert.equal(entries.length, 5, 'wrong number of outline entries'); + assert.equal(entries[0].label, 'root1'); + assert.equal(entries[0].level, 7); + assert.equal(entries[1].label, 'nested1'); + assert.equal(entries[1].level, 8); + assert.equal(entries[2].label, 'nested2'); + assert.equal(entries[2].level, 8); + assert.equal(entries[3].label, 'root2'); + assert.equal(entries[3].level, 7); + assert.equal(entries[4].label, 'nested1'); + assert.equal(entries[4].level, 8); + }); + + test('Multiple Cells with symbols', async function () { + setSymbolsForTextModel([{ name: 'var1', selectionRange: {} }], '$1'); + setSymbolsForTextModel([{ name: 'var2', selectionRange: {} }], '$2'); + const entryFactory = new NotebookOutlineEntryFactory(executionService); + + const cell1 = createCellViewModel(1, '$1'); + const cell2 = createCellViewModel(1, '$2'); + await entryFactory.cacheSymbols(cell1.model.textModel!, outlineModelService, CancellationToken.None); + await entryFactory.cacheSymbols(cell2.model.textModel!, outlineModelService, CancellationToken.None); + + const entries1 = entryFactory.getOutlineEntries(createCellViewModel(1, '$1'), 0); + const entries2 = entryFactory.getOutlineEntries(createCellViewModel(1, '$2'), 0); + + + assert.equal(entries1.length, 1, 'wrong number of outline entries'); + assert.equal(entries1[0].label, 'var1'); + assert.equal(entries2.length, 1, 'wrong number of outline entries'); + assert.equal(entries2[0].label, 'var2'); + }); + +}); diff --git a/code/src/vs/workbench/contrib/notebook/test/browser/notebookStickyScroll.test.ts b/code/src/vs/workbench/contrib/notebook/test/browser/notebookStickyScroll.test.ts index f80bdb205c4..ead402ce540 100644 --- a/code/src/vs/workbench/contrib/notebook/test/browser/notebookStickyScroll.test.ts +++ b/code/src/vs/workbench/contrib/notebook/test/browser/notebookStickyScroll.test.ts @@ -14,7 +14,7 @@ import { TestInstantiationService } from 'vs/platform/instantiation/test/common/ import { NotebookCellOutline } from 'vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline'; import { INotebookEditor, INotebookEditorPane } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { INotebookCellList } from 'vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon'; -import { OutlineEntry } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProvider'; +import { OutlineEntry } from 'vs/workbench/contrib/notebook/browser/viewModel/OutlineEntry'; import { NotebookStickyLine, computeContent } from 'vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll'; import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { createNotebookCellList, setupInstantiationService, withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; diff --git a/code/src/vs/workbench/contrib/search/browser/searchActionsTextQuickAccess.ts b/code/src/vs/workbench/contrib/search/browser/searchActionsTextQuickAccess.ts index b517ae9e2b3..8306eb4af7c 100644 --- a/code/src/vs/workbench/contrib/search/browser/searchActionsTextQuickAccess.ts +++ b/code/src/vs/workbench/contrib/search/browser/searchActionsTextQuickAccess.ts @@ -10,6 +10,10 @@ import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { category } from 'vs/workbench/contrib/search/browser/searchActionsBase'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { TEXT_SEARCH_QUICK_ACCESS_PREFIX } from 'vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IEditor } from 'vs/editor/common/editorCommon'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { getSelectionTextFromEditor } from 'vs/workbench/contrib/search/browser/searchView'; registerAction2(class TextSearchQuickAccessAction extends Action2 { @@ -29,6 +33,28 @@ registerAction2(class TextSearchQuickAccessAction extends Action2 { override async run(accessor: ServicesAccessor, match: RenderableMatch | undefined): Promise { const quickInputService = accessor.get(IQuickInputService); - quickInputService.quickAccess.show(TEXT_SEARCH_QUICK_ACCESS_PREFIX); + const searchText = getSearchText(accessor) ?? ''; + quickInputService.quickAccess.show(TEXT_SEARCH_QUICK_ACCESS_PREFIX + searchText); } }); + +function getSearchText(accessor: ServicesAccessor): string | null { + const editorService = accessor.get(IEditorService); + const configurationService = accessor.get(IConfigurationService); + + const activeEditor: IEditor = editorService.activeTextEditorControl as IEditor; + if (!activeEditor) { + return null; + } + if (!activeEditor.hasTextFocus()) { + return null; + } + + // only happen if it would also happen for the search view + const seedSearchStringFromSelection = configurationService.getValue('editor.find.seedSearchStringFromSelection'); + if (!seedSearchStringFromSelection) { + return null; + } + + return getSelectionTextFromEditor(false, activeEditor); +} diff --git a/code/src/vs/workbench/contrib/search/browser/searchView.ts b/code/src/vs/workbench/contrib/search/browser/searchView.ts index 76d0f4824dd..019f4a9f4ae 100644 --- a/code/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/code/src/vs/workbench/contrib/search/browser/searchView.ts @@ -1288,45 +1288,12 @@ export class SearchView extends ViewPane { } } - if (!isCodeEditor(editor) || !editor.hasModel()) { + if (!editor) { return null; } - const range = editor.getSelection(); - if (!range) { - return null; - } - - if (range.isEmpty() && this.searchConfig.seedWithNearestWord && allowUnselectedWord) { - const wordAtPosition = editor.getModel().getWordAtPosition(range.getStartPosition()); - if (wordAtPosition) { - return wordAtPosition.word; - } - } - - if (!range.isEmpty()) { - let searchText = ''; - for (let i = range.startLineNumber; i <= range.endLineNumber; i++) { - let lineText = editor.getModel().getLineContent(i); - if (i === range.endLineNumber) { - lineText = lineText.substring(0, range.endColumn - 1); - } - - if (i === range.startLineNumber) { - lineText = lineText.substring(range.startColumn - 1); - } - - if (i !== range.startLineNumber) { - lineText = '\n' + lineText; - } - - searchText += lineText; - } - - return searchText; - } - - return null; + const allowUnselected = this.searchConfig.seedWithNearestWord && allowUnselectedWord; + return getSelectionTextFromEditor(allowUnselected, editor); } private showsFileTypes(): boolean { @@ -2178,3 +2145,44 @@ export function getEditorSelectionFromMatch(element: FileMatchOrMatch, viewModel } return undefined; } + +export function getSelectionTextFromEditor(allowUnselectedWord: boolean, editor: IEditor): string | null { + + if (!isCodeEditor(editor) || !editor.hasModel()) { + return null; + } + + const range = editor.getSelection(); + if (!range) { + return null; + } + + if (range.isEmpty()) { + if (allowUnselectedWord) { + const wordAtPosition = editor.getModel().getWordAtPosition(range.getStartPosition()); + return wordAtPosition?.word ?? null; + } else { + return null; + } + } + + let searchText = ''; + for (let i = range.startLineNumber; i <= range.endLineNumber; i++) { + let lineText = editor.getModel().getLineContent(i); + if (i === range.endLineNumber) { + lineText = lineText.substring(0, range.endColumn - 1); + } + + if (i === range.startLineNumber) { + lineText = lineText.substring(range.startColumn - 1); + } + + if (i !== range.startLineNumber) { + lineText = '\n' + lineText; + } + + searchText += lineText; + } + + return searchText; +} diff --git a/code/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/code/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index f0b2cb7d711..a252d8e4814 100644 --- a/code/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/code/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -344,12 +344,13 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if (this._terminalService.getReconnectedTerminals('Task')?.length) { this._attemptTaskReconnection(); } else { - this._register(this._terminalService.onDidChangeConnectionState(async () => { - await this._terminalService.whenConnected; + this._terminalService.whenConnected.then(() => { if (this._terminalService.getReconnectedTerminals('Task')?.length) { this._attemptTaskReconnection(); + } else { + this._tasksReconnected = true; } - })); + }); } this._upgrade(); } @@ -2769,6 +2770,9 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } private async _runTaskCommand(filter?: string | ITaskIdentifier): Promise { + if (!this._tasksReconnected) { + return; + } if (!filter) { return this._doRunTaskCommand(); } @@ -2886,7 +2890,6 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if (executeResult) { return this._handleExecuteResult(executeResult); } else { - this._doRunTaskCommand(); return Promise.resolve(undefined); } }); @@ -3043,6 +3046,9 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } private _runBuildCommand(): void { + if (!this._tasksReconnected) { + return; + } return this._runTaskGroupCommand(TaskGroup.Build, { fetching: nls.localize('TaskService.fetchingBuildTasks', 'Fetching build tasks...'), select: nls.localize('TaskService.pickBuildTask', 'Select the build task to run'), diff --git a/code/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts b/code/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts index 830d2a7277b..263eee32445 100644 --- a/code/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts +++ b/code/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts @@ -936,7 +936,6 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { }); if (trigger === Triggers.reconnect && !!terminal.xterm) { const bufferLines = []; - this._fireTaskEvent(TaskEvent.general(TaskEventKind.Active, task, terminal.instanceId)); const bufferReverseIterator = terminal.xterm.getBufferReverseIterator(); const startRegex = new RegExp(watchingProblemMatcher.beginPatterns.map(pattern => pattern.source).join('|')); for (const nextLine of bufferReverseIterator) { @@ -945,10 +944,17 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { break; } } + let delayer: Async.Delayer | undefined = undefined; for (let i = bufferLines.length - 1; i >= 0; i--) { watchingProblemMatcher.processLine(bufferLines[i]); + if (!delayer) { + delayer = new Async.Delayer(3000); + } + delayer.trigger(() => { + watchingProblemMatcher.forceDelivery(); + delayer = undefined; + }); } - this._fireTaskEvent(TaskEvent.general(TaskEventKind.Inactive, task, terminal.instanceId)); } } else { [terminal, error] = await this._createTerminal(task, resolver, workspaceFolder); diff --git a/code/src/vs/workbench/services/extensions/common/abstractExtensionService.ts b/code/src/vs/workbench/services/extensions/common/abstractExtensionService.ts index 34184b08206..ccb729ce206 100644 --- a/code/src/vs/workbench/services/extensions/common/abstractExtensionService.ts +++ b/code/src/vs/workbench/services/extensions/common/abstractExtensionService.ts @@ -510,7 +510,13 @@ export abstract class AbstractExtensionService extends Disposable implements IEx let exitCode: number; try { exitCode = await extensionHostManager.extensionTestsExecute(); + if (isCI) { + this._logService.info(`Extension host test runner exit code: ${exitCode}`); + } } catch (err) { + if (isCI) { + this._logService.error(`Extension host test runner error`, err); + } console.error(err); exitCode = 1 /* ERROR */; } diff --git a/code/src/vs/workbench/services/extensions/electron-sandbox/nativeExtensionService.ts b/code/src/vs/workbench/services/extensions/electron-sandbox/nativeExtensionService.ts index 13e18b9e611..d63228db9d2 100644 --- a/code/src/vs/workbench/services/extensions/electron-sandbox/nativeExtensionService.ts +++ b/code/src/vs/workbench/services/extensions/electron-sandbox/nativeExtensionService.ts @@ -436,6 +436,9 @@ export class NativeExtensionService extends AbstractExtensionService implements if (parseExtensionDevOptions(this._environmentService).isExtensionDevTestFromCli) { // When CLI testing make sure to exit with proper exit code + if (isCI) { + this._logService.info(`Asking native host service to exit with code ${code}.`); + } this._nativeHostService.exit(code); } else { // Expected development extension termination: When the extension host goes down we also shutdown the window diff --git a/code/src/vscode-dts/vscode.d.ts b/code/src/vscode-dts/vscode.d.ts index cb6a6cabf48..b5c09fe1617 100644 --- a/code/src/vscode-dts/vscode.d.ts +++ b/code/src/vscode-dts/vscode.d.ts @@ -17995,19 +17995,4 @@ declare module 'vscode' { * enables reusing existing code without migrating to a specific promise implementation. Still, * we recommend the use of native promises which are available in this editor. */ -interface Thenable { - /** - * Attaches callbacks for the resolution and/or rejection of the Promise. - * @param onfulfilled The callback to execute when the Promise is resolved. - * @param onrejected The callback to execute when the Promise is rejected. - * @returns A Promise for the completion of which ever callback is executed. - */ - then(onfulfilled?: (value: T) => TResult | Thenable, onrejected?: (reason: any) => TResult | Thenable): Thenable; - /** - * Attaches callbacks for the resolution and/or rejection of the Promise. - * @param onfulfilled The callback to execute when the Promise is resolved. - * @param onrejected The callback to execute when the Promise is rejected. - * @returns A Promise for the completion of which ever callback is executed. - */ - then(onfulfilled?: (value: T) => TResult | Thenable, onrejected?: (reason: any) => void): Thenable; -} +interface Thenable extends PromiseLike { }