diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index 8f6ce9b030d5a..dea532739cb50 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -203,6 +203,7 @@ "--vscode-editorBracketHighlight-unexpectedBracket-foreground", "--vscode-editorBracketMatch-background", "--vscode-editorBracketMatch-border", + "--vscode-editorBracketMatch-foreground", "--vscode-editorBracketPairGuide-activeBackground1", "--vscode-editorBracketPairGuide-activeBackground2", "--vscode-editorBracketPairGuide-activeBackground3", diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index 285d76ee55bb0..18b49fcb268b2 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -80,6 +80,7 @@ export interface Worktree { readonly name: string; readonly path: string; readonly ref: string; + readonly main: boolean; readonly detached: boolean; } diff --git a/extensions/git/src/artifactProvider.ts b/extensions/git/src/artifactProvider.ts index f99e262b9c43f..f63899efa3edb 100644 --- a/extensions/git/src/artifactProvider.ts +++ b/extensions/git/src/artifactProvider.ts @@ -6,7 +6,7 @@ import { LogOutputChannel, SourceControlArtifactProvider, SourceControlArtifactGroup, SourceControlArtifact, Event, EventEmitter, ThemeIcon, l10n, workspace, Uri, Disposable, Command } from 'vscode'; import { coalesce, dispose, filterEvent, IDisposable, isCopilotWorktree } from './util'; import { Repository } from './repository'; -import { Commit, Ref, RefType } from './api/git'; +import { Ref, RefType, Worktree } from './api/git'; import { OperationKind } from './operation'; /** @@ -55,11 +55,14 @@ function sortRefByName(refA: Ref, refB: Ref): number { return 0; } -function sortByCommitDateDesc(a: { commitDetails?: Commit }, b: { commitDetails?: Commit }): number { - const aCommitDate = a.commitDetails?.commitDate?.getTime() ?? 0; - const bCommitDate = b.commitDetails?.commitDate?.getTime() ?? 0; - - return bCommitDate - aCommitDate; +function sortByWorktreeTypeAndNameAsc(a: Worktree, b: Worktree): number { + if (a.main && !b.main) { + return -1; + } else if (!a.main && b.main) { + return 1; + } else { + return a.name.localeCompare(b.name); + } } export class GitArtifactProvider implements SourceControlArtifactProvider, IDisposable { @@ -164,7 +167,7 @@ export class GitArtifactProvider implements SourceControlArtifactProvider, IDisp } else if (group === 'worktrees') { const worktrees = await this.repository.getWorktreeDetails(); - return worktrees.sort(sortByCommitDateDesc).map(w => ({ + return worktrees.sort(sortByWorktreeTypeAndNameAsc).map(w => ({ id: w.path, name: w.name, description: coalesce([ @@ -172,10 +175,11 @@ export class GitArtifactProvider implements SourceControlArtifactProvider, IDisp w.commitDetails?.hash.substring(0, shortCommitLength), w.commitDetails?.message.split('\n')[0] ]).join(' \u2022 '), - icon: isCopilotWorktree(w.path) - ? new ThemeIcon('chat-sparkle') - : new ThemeIcon('worktree'), - timestamp: w.commitDetails?.commitDate?.getTime(), + icon: w.main + ? new ThemeIcon('repo') + : isCopilotWorktree(w.path) + ? new ThemeIcon('chat-sparkle') + : new ThemeIcon('worktree') })); } } catch (err) { diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 012710ca9f7c9..5b2ca69d08739 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -29,6 +29,7 @@ export interface IDotGit { readonly path: string; readonly commonPath?: string; readonly superProjectPath?: string; + readonly isBare: boolean; } export interface IFileStatus { @@ -575,7 +576,12 @@ export class Git { commonDotGitPath = path.normalize(commonDotGitPath); } + const raw = await fs.readFile(path.join(commonDotGitPath ?? dotGitPath, 'config'), 'utf8'); + const coreSections = GitConfigParser.parse(raw).find(s => s.name === 'core'); + const isBare = coreSections?.properties['bare'] === 'true'; + return { + isBare, path: dotGitPath, commonPath: commonDotGitPath !== dotGitPath ? commonDotGitPath : undefined, superProjectPath: superProjectPath ? path.normalize(superProjectPath) : undefined @@ -2954,10 +2960,27 @@ export class Repository { private async getWorktreesFS(): Promise { try { // List all worktree folder names - const worktreesPath = path.join(this.dotGit.commonPath ?? this.dotGit.path, 'worktrees'); + const mainRepositoryPath = this.dotGit.commonPath ?? this.dotGit.path; + const worktreesPath = path.join(mainRepositoryPath, 'worktrees'); const dirents = await fs.readdir(worktreesPath, { withFileTypes: true }); const result: Worktree[] = []; + if (!this.dotGit.isBare) { + // Add main worktree for a non-bare repository + const headPath = path.join(mainRepositoryPath, 'HEAD'); + const headContent = (await fs.readFile(headPath, 'utf8')).trim(); + + const mainRepositoryWorktreeName = path.basename(path.dirname(mainRepositoryPath)); + + result.push({ + name: mainRepositoryWorktreeName, + path: path.dirname(mainRepositoryPath), + ref: headContent.replace(/^ref: /, ''), + detached: !headContent.startsWith('ref: '), + main: true + } satisfies Worktree); + } + for (const dirent of dirents) { if (!dirent.isDirectory()) { continue; @@ -2977,7 +3000,8 @@ export class Repository { // Remove 'ref: ' prefix ref: headContent.replace(/^ref: /, ''), // Detached if HEAD does not start with 'ref: ' - detached: !headContent.startsWith('ref: ') + detached: !headContent.startsWith('ref: '), + main: false }); } catch (err) { if (/ENOENT/.test(err.message)) { diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 7ca1c8861949d..8480e6d361764 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -15,7 +15,7 @@ import { Branch, BranchQuery, Change, CommitOptions, DiffChange, FetchOptions, F import { AutoFetcher } from './autofetch'; import { GitBranchProtectionProvider, IBranchProtectionProviderRegistry } from './branchProtection'; import { debounce, memoize, sequentialize, throttle } from './decorators'; -import { Repository as BaseRepository, BlameInformation, Commit, CommitShortStat, GitError, LogFileOptions, LsTreeElement, PullOptions, RefQuery, Stash, Submodule, Worktree } from './git'; +import { Repository as BaseRepository, BlameInformation, Commit, CommitShortStat, GitError, IDotGit, LogFileOptions, LsTreeElement, PullOptions, RefQuery, Stash, Submodule, Worktree } from './git'; import { GitHistoryProvider } from './historyProvider'; import { Operation, OperationKind, OperationManager, OperationResult } from './operation'; import { CommitCommandsCenter, IPostCommitCommandsProviderRegistry } from './postCommitCommands'; @@ -866,7 +866,7 @@ export class Repository implements Disposable { return this.repository.rootRealPath; } - get dotGit(): { path: string; commonPath?: string } { + get dotGit(): IDotGit { return this.repository.dotGit; } diff --git a/extensions/vb/language-configuration.json b/extensions/vb/language-configuration.json index e5f718360f49a..734448101e081 100644 --- a/extensions/vb/language-configuration.json +++ b/extensions/vb/language-configuration.json @@ -32,14 +32,14 @@ "flags": "i" }, "increaseIndentPattern": { - "pattern": "^\\s*((If|ElseIf).*Then(?!\\s+(End\\s+If))\\s*(('|REM).*)?$)|\\b(Else|While|For|Do|Select\\s+Case|Case|Sub|Function|Class|Module|Enum|Structure|Interface|Namespace|With|Try|Catch|Finally|SyncLock|Using|Property|Get|Set|AddHandler|RaiseEvent|RemoveHandler|Event|Operator)\\b(?!.*\\bEnd\\s+(If|Sub|Function|Class|Module|Enum|Structure|Interface|Namespace|With|Select|Try|While|For|Property|Get|Set|SyncLock|Using|AddHandler|RaiseEvent|RemoveHandler|Event|Operator)\\b).*(('|REM).*)?$", + "pattern": "^\\s*((If|ElseIf).*Then(?!.*End\\s+If)\\s*(('|REM).*)?|(Else|While|For|Do|Select\\s+Case|Case|Sub|Function|Class|Module|Enum|Structure|Interface|Namespace|With|Try|Catch|Finally|SyncLock|Using|Property|Get|Set|AddHandler|RaiseEvent|RemoveHandler|Event|Operator)\\b(?!.*\\bEnd\\s+(If|Sub|Function|Class|Module|Enum|Structure|Interface|Namespace|With|Select|Try|While|For|Property|Get|Set|SyncLock|Using|AddHandler|RaiseEvent|RemoveHandler|Event|Operator)\\b).*(('|REM).*)?)$", "flags": "i" } }, "onEnterRules": [ - // Prevent indent after End statements, block terminators (Else, ElseIf, Loop, Next, etc.) + // Prevent indent after End statements and block terminators (Loop, Next, etc.) { - "beforeText": { "pattern": "^\\s*((End\\s+(If|Sub|Function|Class|Module|Enum|Structure|Interface|Namespace|With|Select|Try|While|For|Property|Get|Set|SyncLock|Using|AddHandler|RaiseEvent|RemoveHandler|Event|Operator))|Else|ElseIf|Loop|Next|Wend|Until)\\b.*$", "flags": "i" }, + "beforeText": { "pattern": "^\\s*((End\\s+(If|Sub|Function|Class|Module|Enum|Structure|Interface|Namespace|With|Select|Try|While|For|Property|Get|Set|SyncLock|Using|AddHandler|RaiseEvent|RemoveHandler|Event|Operator))|Loop|Next|Wend|Until)\\b.*$", "flags": "i" }, "action": { "indent": "none" } @@ -47,15 +47,7 @@ // Prevent indent when pressing Enter on a blank line after End statements or block terminators { "beforeText": "^\\s*$", - "previousLineText": { "pattern": "^\\s*((End\\s+(If|Sub|Function|Class|Module|Enum|Structure|Interface|Namespace|With|Select|Try|While|For|Property|Get|Set|SyncLock|Using|AddHandler|RaiseEvent|RemoveHandler|Event|Operator))|Else|ElseIf|Loop|Next|Wend|Until)\\b.*$", "flags": "i" }, - "action": { - "indent": "none" - } - }, - // Prevent indent after lines ending with closing parenthesis (e.g., function calls, method invocations) - { - "beforeText": { "pattern": "^[^'\"]*\\)\\s*('.*)?$", "flags": "i" }, - "afterText": "^(?!\\s*\\))", + "previousLineText": { "pattern": "^\\s*((End\\s+(If|Sub|Function|Class|Module|Enum|Structure|Interface|Namespace|With|Select|Try|While|For|Property|Get|Set|SyncLock|Using|AddHandler|RaiseEvent|RemoveHandler|Event|Operator))|Loop|Next|Wend|Until)\\b.*$", "flags": "i" }, "action": { "indent": "none" } diff --git a/package.json b/package.json index a4a014d7ae22c..c985bd3f1cc50 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.109.0", - "distro": "9ac7c0b1d7f8b73f10dc974777bccc7b55ee60d4", + "distro": "f84811280304020eab0bbc930e85b8f2180b1ed6", "author": { "name": "Microsoft Corporation" }, @@ -240,4 +240,4 @@ "optionalDependencies": { "windows-foreground-love": "0.5.0" } -} +} \ No newline at end of file diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index 10276931dd5fa..f8d394dfafead 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -208,18 +208,7 @@ export interface IProductConfiguration { readonly msftInternalDomains?: string[]; readonly linkProtectionTrustedDomains?: readonly string[]; - readonly defaultAccount?: { - readonly authenticationProvider: { - readonly id: string; - readonly enterpriseProviderId: string; - readonly enterpriseProviderConfig: string; - readonly enterpriseProviderUriSetting: string; - readonly scopes: string[][]; - }; - readonly tokenEntitlementUrl: string; - readonly chatEntitlementUrl: string; - readonly mcpRegistryDataUrl: string; - }; + readonly defaultAccount?: IDefaultAccountConfig; readonly authClientIdMetadataUrl?: string; readonly 'configurationSync.store'?: ConfigurationSyncStore; @@ -242,6 +231,20 @@ export interface IProductConfiguration { readonly extensionConfigurationPolicy?: IStringDictionary; } +export interface IDefaultAccountConfig { + readonly preferredExtensions: string[]; + readonly authenticationProvider: { + readonly id: string; + readonly enterpriseProviderId: string; + readonly enterpriseProviderConfig: string; + readonly enterpriseProviderUriSetting: string; + readonly scopes: string[][]; + }; + readonly tokenEntitlementUrl: string; + readonly chatEntitlementUrl: string; + readonly mcpRegistryDataUrl: string; +} + export interface ITunnelApplicationConfig { authenticationProviders: IStringDictionary<{ scopes: string[] }>; editorWebUrl: string; diff --git a/src/vs/editor/browser/view/viewController.ts b/src/vs/editor/browser/view/viewController.ts index 935bcc5dbb04a..cd312c962716d 100644 --- a/src/vs/editor/browser/view/viewController.ts +++ b/src/vs/editor/browser/view/viewController.ts @@ -14,6 +14,8 @@ import { IViewModel } from '../../common/viewModel.js'; import { IMouseWheelEvent } from '../../../base/browser/mouseEvent.js'; import { EditorOption } from '../../common/config/editorOptions.js'; import * as platform from '../../../base/common/platform.js'; +import { StandardTokenType } from '../../common/encodedTokenAttributes.js'; +import { ITextModel } from '../../common/model.js'; export interface IMouseDispatchData { position: Position; @@ -129,6 +131,67 @@ export class ViewController { } } + /** + * Selects content inside brackets if the position is right after an opening bracket or right before a closing bracket. + * @param pos The position in the model. + * @param model The text model. + */ + private static _trySelectBracketContent(model: ITextModel, pos: Position): Selection | undefined { + // Try to find bracket match if we're right after an opening bracket. + if (pos.column > 1) { + const pair = model.bracketPairs.matchBracket(pos.with(undefined, pos.column - 1)); + if (pair && pair[0].getEndPosition().equals(pos)) { + return Selection.fromPositions(pair[0].getEndPosition(), pair[1].getStartPosition()); + } + } + + // Try to find bracket match if we're right before a closing bracket. + if (pos.column <= model.getLineMaxColumn(pos.lineNumber)) { + const pair = model.bracketPairs.matchBracket(pos); + if (pair && pair[1].getStartPosition().equals(pos)) { + return Selection.fromPositions(pair[0].getEndPosition(), pair[1].getStartPosition()); + } + } + + return undefined; + } + + /** + * Selects content inside a string if the position is right after an opening quote or right before a closing quote. + * @param pos The position in the model. + * @param model The text model. + */ + private static _trySelectStringContent(model: ITextModel, pos: Position): Selection | undefined { + const { lineNumber, column } = pos; + const { tokenization: tokens } = model; + + // Ensure we have accurate tokens for the line. + if (!tokens.hasAccurateTokensForLine(lineNumber)) { + if (tokens.isCheapToTokenize(lineNumber)) { + tokens.forceTokenization(lineNumber); + } else { + return undefined; + } + } + + // Check if current token is a string. + const lineTokens = tokens.getLineTokens(lineNumber); + const index = lineTokens.findTokenIndexAtOffset(column - 1); + if (lineTokens.getStandardTokenType(index) !== StandardTokenType.String) { + return undefined; + } + + // Get 1-based boundaries of the string content (excluding quotes). + const start = lineTokens.getStartOffset(index) + 2; + const end = lineTokens.getEndOffset(index); + + if (column !== start && column !== end) { + return undefined; + } + + return new Selection(lineNumber, start, lineNumber, end); + } + public dispatchMouse(data: IMouseDispatchData): void { const options = this.configuration.options; const selectionClipboardIsOn = (platform.isLinux && options.get(EditorOption.selectionClipboard)); @@ -179,7 +242,14 @@ export class ViewController { if (data.inSelectionMode) { this._wordSelectDrag(data.position, data.revealType); } else { - this._wordSelect(data.position, data.revealType); + const model = this.viewModel.model; + const modelPos = this._convertViewToModelPosition(data.position); + const selection = ViewController._trySelectBracketContent(model, modelPos) || ViewController._trySelectStringContent(model, modelPos); + if (selection) { + this._select(selection); + } else { + this._wordSelect(data.position, data.revealType); + } } } } @@ -286,6 +356,10 @@ export class ViewController { CoreNavigationCommands.LastCursorLineSelectDrag.runCoreEditorCommand(this.viewModel, this._usualArgs(viewPosition, revealType)); } + private _select(selection: Selection): void { + CoreNavigationCommands.SetSelection.runCoreEditorCommand(this.viewModel, { source: 'mouse', selection }); + } + private _selectAll(): void { CoreNavigationCommands.SelectAll.runCoreEditorCommand(this.viewModel, { source: 'mouse' }); } diff --git a/src/vs/editor/common/core/editorColorRegistry.ts b/src/vs/editor/common/core/editorColorRegistry.ts index dc5e5e7f7c8e5..e71205d88c27d 100644 --- a/src/vs/editor/common/core/editorColorRegistry.ts +++ b/src/vs/editor/common/core/editorColorRegistry.ts @@ -55,6 +55,7 @@ export const editorCodeLensForeground = registerColor('editorCodeLens.foreground export const editorBracketMatchBackground = registerColor('editorBracketMatch.background', { dark: '#0064001a', light: '#0064001a', hcDark: '#0064001a', hcLight: '#0000' }, nls.localize('editorBracketMatchBackground', 'Background color behind matching brackets')); export const editorBracketMatchBorder = registerColor('editorBracketMatch.border', { dark: '#888', light: '#B9B9B9', hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('editorBracketMatchBorder', 'Color for matching brackets boxes')); +export const editorBracketMatchForeground = registerColor('editorBracketMatch.foreground', null, nls.localize('editorBracketMatchForeground', 'Foreground color for matching brackets')); export const editorOverviewRulerBorder = registerColor('editorOverviewRuler.border', { dark: '#7f7f7f4d', light: '#7f7f7f4d', hcDark: '#7f7f7f4d', hcLight: '#666666' }, nls.localize('editorOverviewRulerBorder', 'Color of the overview ruler border.')); export const editorOverviewRulerBackground = registerColor('editorOverviewRuler.background', null, nls.localize('editorOverviewRulerBackground', 'Background color of the editor overview ruler.')); diff --git a/src/vs/editor/contrib/bracketMatching/browser/bracketMatching.ts b/src/vs/editor/contrib/bracketMatching/browser/bracketMatching.ts index 7e64b4e28c831..5c518d867d562 100644 --- a/src/vs/editor/contrib/bracketMatching/browser/bracketMatching.ts +++ b/src/vs/editor/contrib/bracketMatching/browser/bracketMatching.ts @@ -21,7 +21,8 @@ import * as nls from '../../../../nls.js'; import { MenuId, MenuRegistry } from '../../../../platform/actions/common/actions.js'; import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import { registerColor } from '../../../../platform/theme/common/colorRegistry.js'; -import { themeColorFromId } from '../../../../platform/theme/common/themeService.js'; +import { registerThemingParticipant, themeColorFromId } from '../../../../platform/theme/common/themeService.js'; +import { editorBracketMatchForeground } from '../../../common/core/editorColorRegistry.js'; const overviewRulerBracketMatchForeground = registerColor('editorOverviewRuler.bracketMatchForeground', '#A0A0A0', nls.localize('overviewRulerBracketMatchForeground', 'Overview ruler marker color for matching brackets.')); @@ -300,6 +301,7 @@ export class BracketMatchingController extends Disposable implements IEditorCont description: 'bracket-match-overview', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'bracket-match', + inlineClassName: 'bracket-match-inline', overviewRuler: { color: themeColorFromId(overviewRulerBracketMatchForeground), position: OverviewRulerLane.Center @@ -309,7 +311,8 @@ export class BracketMatchingController extends Disposable implements IEditorCont private static readonly _DECORATION_OPTIONS_WITHOUT_OVERVIEW_RULER = ModelDecorationOptions.register({ description: 'bracket-match-no-overview', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, - className: 'bracket-match' + className: 'bracket-match', + inlineClassName: 'bracket-match-inline' }); private _updateBrackets(): void { @@ -414,3 +417,13 @@ MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { }, order: 2 }); + +// Theming participant to ensure bracket-match color overrides bracket pair colorization +registerThemingParticipant((theme, collector) => { + const bracketMatchForeground = theme.getColor(editorBracketMatchForeground); + if (bracketMatchForeground) { + // Use higher specificity to override bracket pair colorization + // Apply color to inline class to avoid layout jumps + collector.addRule(`.monaco-editor .bracket-match-inline { color: ${bracketMatchForeground} !important; }`); + } +}); diff --git a/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts b/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts index 98558263b73dd..4359d30fffaaf 100644 --- a/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts +++ b/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts @@ -19,7 +19,7 @@ import { AutoIndentOnPaste, IndentationToSpacesCommand, IndentationToTabsCommand import { withTestCodeEditor } from '../../../../test/browser/testCodeEditor.js'; import { testCommand } from '../../../../test/browser/testCommand.js'; import { goIndentationRules, htmlIndentationRules, javascriptIndentationRules, latexIndentationRules, luaIndentationRules, phpIndentationRules, rubyIndentationRules, vbIndentationRules } from '../../../../test/common/modes/supports/indentationRules.js'; -import { cppOnEnterRules, htmlOnEnterRules, javascriptOnEnterRules, phpOnEnterRules } from '../../../../test/common/modes/supports/onEnterRules.js'; +import { cppOnEnterRules, htmlOnEnterRules, javascriptOnEnterRules, phpOnEnterRules, vbOnEnterRules } from '../../../../test/common/modes/supports/onEnterRules.js'; import { TypeOperations } from '../../../../common/cursor/cursorTypeOperations.js'; import { cppBracketRules, goBracketRules, htmlBracketRules, latexBracketRules, luaBracketRules, phpBracketRules, rubyBracketRules, typescriptBracketRules, vbBracketRules } from '../../../../test/common/modes/supports/bracketRules.js'; import { javascriptAutoClosingPairsRules, latexAutoClosingPairsRules } from '../../../../test/common/modes/supports/autoClosingPairsRules.js'; @@ -95,6 +95,7 @@ export function registerLanguageConfiguration(languageConfigurationService: ILan return languageConfigurationService.register(language, { brackets: vbBracketRules, indentationRules: vbIndentationRules, + onEnterRules: vbOnEnterRules, }); case Language.Latex: return languageConfigurationService.register(language, { @@ -1759,6 +1760,597 @@ suite('Auto Indent On Type - Visual Basic', () => { ].join('\n')); }); }); + + test('issue #118932: indent after Module declaration', () => { + + // https://github.com/microsoft/vscode/issues/118932 + + const model = createTextModel('', languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel) => { + viewModel.type('Module Test'); + viewModel.type('\n', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Module Test', + ' ', + ].join('\n')); + }); + }); + + test('issue #118932: indent after Sub declaration', () => { + + // https://github.com/microsoft/vscode/issues/118932 + + const model = createTextModel([ + 'Module Test', + ' ', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(2, 5, 2, 5)); + viewModel.type('Sub Main()'); + viewModel.type('\n', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Module Test', + ' Sub Main()', + ' ', + ].join('\n')); + }); + }); + + test('issue #118932: dedent on End Sub', () => { + + // https://github.com/microsoft/vscode/issues/118932 + + const model = createTextModel([ + 'Module Test', + ' Sub Main()', + ' Console.WriteLine("Hello")', + ' End Su', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(4, 15, 4, 15)); + viewModel.type('b', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Module Test', + ' Sub Main()', + ' Console.WriteLine("Hello")', + ' End Sub', + ].join('\n')); + }); + }); + + test('issue #118932: dedent on End Module', () => { + + // https://github.com/microsoft/vscode/issues/118932 + // When End Module is typed right after Module (no nested blocks), it dedents correctly + + const model = createTextModel([ + 'Module Test', + ' Private x As Integer', + ' End Modul', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(3, 14, 3, 14)); + viewModel.type('e', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Module Test', + ' Private x As Integer', + 'End Module', + ].join('\n')); + }); + }); + + test('issue #118932: indent after Function declaration', () => { + + // https://github.com/microsoft/vscode/issues/118932 + + const model = createTextModel([ + 'Module Test', + ' ', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(2, 5, 2, 5)); + viewModel.type('Function Add(a As Integer, b As Integer) As Integer'); + viewModel.type('\n', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Module Test', + ' Function Add(a As Integer, b As Integer) As Integer', + ' ', + ].join('\n')); + }); + }); + + test('issue #118932: dedent on End Function', () => { + + // https://github.com/microsoft/vscode/issues/118932 + + const model = createTextModel([ + 'Module Test', + ' Function Add(a, b)', + ' Return a + b', + ' End Functio', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(4, 20, 4, 20)); + viewModel.type('n', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Module Test', + ' Function Add(a, b)', + ' Return a + b', + ' End Function', + ].join('\n')); + }); + }); + + test('issue #118932: indent after If Then', () => { + + // https://github.com/microsoft/vscode/issues/118932 + + const model = createTextModel([ + 'Sub Test()', + ' ', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(2, 5, 2, 5)); + viewModel.type('If x > 0 Then'); + viewModel.type('\n', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Sub Test()', + ' If x > 0 Then', + ' ', + ].join('\n')); + }); + }); + + test('issue #118932: indent after ElseIf Then', () => { + + // https://github.com/microsoft/vscode/issues/118932 + + const model = createTextModel([ + 'Sub Test()', + ' If x > 0 Then', + ' DoSomething()', + ' ElseIf x < 0 Then', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(4, 22, 4, 22)); + viewModel.type('\n', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Sub Test()', + ' If x > 0 Then', + ' DoSomething()', + ' ElseIf x < 0 Then', + ' ', + ].join('\n')); + }); + }); + + test('issue #118932: dedent and indent on Else', () => { + + // https://github.com/microsoft/vscode/issues/118932 + + const model = createTextModel([ + 'Sub Test()', + ' If x > 0 Then', + ' DoSomething()', + ' Els', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(4, 12, 4, 12)); + viewModel.type('e', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Sub Test()', + ' If x > 0 Then', + ' DoSomething()', + ' Else', + ].join('\n')); + }); + }); + + test('issue #118932: indent after While', () => { + + // https://github.com/microsoft/vscode/issues/118932 + + const model = createTextModel([ + 'Sub Test()', + ' ', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(2, 5, 2, 5)); + viewModel.type('While x > 0'); + viewModel.type('\n', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Sub Test()', + ' While x > 0', + ' ', + ].join('\n')); + }); + }); + + test('issue #118932: dedent on End While', () => { + + // https://github.com/microsoft/vscode/issues/118932 + + const model = createTextModel([ + 'Sub Test()', + ' While x > 0', + ' x = x - 1', + ' End Whil', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(4, 17, 4, 17)); + viewModel.type('e', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Sub Test()', + ' While x > 0', + ' x = x - 1', + ' End While', + ].join('\n')); + }); + }); + + test('issue #118932: indent after For', () => { + + // https://github.com/microsoft/vscode/issues/118932 + + const model = createTextModel([ + 'Sub Test()', + ' ', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(2, 5, 2, 5)); + viewModel.type('For i = 1 To 10'); + viewModel.type('\n', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Sub Test()', + ' For i = 1 To 10', + ' ', + ].join('\n')); + }); + }); + + test('issue #118932: dedent on Next', () => { + + // https://github.com/microsoft/vscode/issues/118932 + + const model = createTextModel([ + 'Sub Test()', + ' For i = 1 To 10', + ' DoSomething(i)', + ' Nex', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(4, 12, 4, 12)); + viewModel.type('t', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Sub Test()', + ' For i = 1 To 10', + ' DoSomething(i)', + ' Next', + ].join('\n')); + }); + }); + + test('issue #118932: indent after Do', () => { + + // https://github.com/microsoft/vscode/issues/118932 + + const model = createTextModel([ + 'Sub Test()', + ' ', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(2, 5, 2, 5)); + viewModel.type('Do'); + viewModel.type('\n', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Sub Test()', + ' Do', + ' ', + ].join('\n')); + }); + }); + + test('issue #118932: dedent on Loop', () => { + + // https://github.com/microsoft/vscode/issues/118932 + + const model = createTextModel([ + 'Sub Test()', + ' Do', + ' x = x + 1', + ' Loo', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(4, 12, 4, 12)); + viewModel.type('p', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Sub Test()', + ' Do', + ' x = x + 1', + ' Loop', + ].join('\n')); + }); + }); + + test('issue #118932: indent after Select Case', () => { + + // https://github.com/microsoft/vscode/issues/118932 + + const model = createTextModel([ + 'Sub Test()', + ' ', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(2, 5, 2, 5)); + viewModel.type('Select Case x'); + viewModel.type('\n', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Sub Test()', + ' Select Case x', + ' ', + ].join('\n')); + }); + }); + + test('issue #118932: dedent on End Select', () => { + + // https://github.com/microsoft/vscode/issues/118932 + // When End Select is typed, it dedents to match Select Case level + + const model = createTextModel([ + 'Sub Test()', + ' Select Case x', + ' End Selec', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(3, 18, 3, 18)); + viewModel.type('t', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Sub Test()', + ' Select Case x', + ' End Select', + ].join('\n')); + }); + }); + + test('issue #118932: indent after Try', () => { + + // https://github.com/microsoft/vscode/issues/118932 + + const model = createTextModel([ + 'Sub Test()', + ' ', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(2, 5, 2, 5)); + viewModel.type('Try'); + viewModel.type('\n', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Sub Test()', + ' Try', + ' ', + ].join('\n')); + }); + }); + + test('issue #118932: dedent and indent on Catch', () => { + + // https://github.com/microsoft/vscode/issues/118932 + + const model = createTextModel([ + 'Sub Test()', + ' Try', + ' DoSomething()', + ' Catc', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(4, 13, 4, 13)); + viewModel.type('h', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Sub Test()', + ' Try', + ' DoSomething()', + ' Catch', + ].join('\n')); + }); + }); + + test('issue #118932: dedent and indent on Finally', () => { + + // https://github.com/microsoft/vscode/issues/118932 + + const model = createTextModel([ + 'Sub Test()', + ' Try', + ' DoSomething()', + ' Catch', + ' HandleError()', + ' Finall', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(6, 15, 6, 15)); + viewModel.type('y', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Sub Test()', + ' Try', + ' DoSomething()', + ' Catch', + ' HandleError()', + ' Finally', + ].join('\n')); + }); + }); + + test('issue #118932: dedent on End Try', () => { + + // https://github.com/microsoft/vscode/issues/118932 + + const model = createTextModel([ + 'Sub Test()', + ' Try', + ' DoSomething()', + ' Catch', + ' HandleError()', + ' Finally', + ' Cleanup()', + ' End Tr', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(8, 15, 8, 15)); + viewModel.type('y', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Sub Test()', + ' Try', + ' DoSomething()', + ' Catch', + ' HandleError()', + ' Finally', + ' Cleanup()', + ' End Try', + ].join('\n')); + }); + }); + + test('issue #118932: indent after Class', () => { + + // https://github.com/microsoft/vscode/issues/118932 + + const model = createTextModel('', languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel) => { + viewModel.type('Class MyClass'); + viewModel.type('\n', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Class MyClass', + ' ', + ].join('\n')); + }); + }); + + test('issue #118932: dedent on End Class', () => { + + // https://github.com/microsoft/vscode/issues/118932 + + const model = createTextModel([ + 'Class MyClass', + ' Private x As Integer', + ' End Clas', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(3, 14, 3, 14)); + viewModel.type('s', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Class MyClass', + ' Private x As Integer', + 'End Class', + ].join('\n')); + }); + }); + + test('issue #118932: full program indentation flow', () => { + + // https://github.com/microsoft/vscode/issues/118932 + // Verify the complete flow as described in the verification comment + // Note: Auto-indent only triggers on typing the last character that completes a keyword + // and only decreases by one indentation level per keyword completion + + const model = createTextModel('', languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel) => { + // Type Module Test and press Enter + viewModel.type('Module Test'); + viewModel.type('\n', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Module Test', + ' ', + ].join('\n'), 'After Module Test'); + + // Type Sub Main() and press Enter + viewModel.type('Sub Main()'); + viewModel.type('\n', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Module Test', + ' Sub Main()', + ' ', + ].join('\n'), 'After Sub Main()'); + + // Type Console.WriteLine and press Enter + viewModel.type('Console.WriteLine("Hello, World!")'); + viewModel.type('\n', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Module Test', + ' Sub Main()', + ' Console.WriteLine("Hello, World!")', + ' ', + ].join('\n'), 'After Console.WriteLine'); + + // Type End Su then 'b' to complete End Sub (auto-indent triggers on last char) + viewModel.type('End Su'); + viewModel.type('b', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Module Test', + ' Sub Main()', + ' Console.WriteLine("Hello, World!")', + ' End Sub', + ].join('\n'), 'After End Sub'); + + // Press Enter - should maintain same indent level after End Sub + viewModel.type('\n', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Module Test', + ' Sub Main()', + ' Console.WriteLine("Hello, World!")', + ' End Sub', + ' ', + ].join('\n'), 'After Enter after End Sub'); + }); + }); }); diff --git a/src/vs/editor/standalone/browser/services/standaloneWebWorkerService.ts b/src/vs/editor/standalone/browser/services/standaloneWebWorkerService.ts index 9e60c93dfdea1..b5a676fd8702e 100644 --- a/src/vs/editor/standalone/browser/services/standaloneWebWorkerService.ts +++ b/src/vs/editor/standalone/browser/services/standaloneWebWorkerService.ts @@ -22,6 +22,11 @@ export class StandaloneWebWorkerService extends WebWorkerService { return super._createWorker(descriptor); } + protected override _getWorkerLoadingFailedErrorMessage(descriptor: WebWorkerDescriptor): string | undefined { + return `Failed to load worker script for label: ${descriptor.label}. +Ensure your bundler properly bundles modules referenced by "new URL("...?esm", import.meta.url)".`; + } + override getWorkerUrl(descriptor: WebWorkerDescriptor): string { const monacoEnvironment = getMonacoEnvironment(); if (monacoEnvironment) { diff --git a/src/vs/editor/test/browser/view/viewController.test.ts b/src/vs/editor/test/browser/view/viewController.test.ts new file mode 100644 index 0000000000000..a3b26791ce88c --- /dev/null +++ b/src/vs/editor/test/browser/view/viewController.test.ts @@ -0,0 +1,147 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { DisposableStore } from '../../../../base/common/lifecycle.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; +import { TestInstantiationService } from '../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { TestThemeService } from '../../../../platform/theme/test/common/testThemeService.js'; +import { NavigationCommandRevealType } from '../../../browser/coreCommands.js'; +import { ViewController } from '../../../browser/view/viewController.js'; +import { ViewUserInputEvents } from '../../../browser/view/viewUserInputEvents.js'; +import { Position } from '../../../common/core/position.js'; +import { ILanguageService } from '../../../common/languages/language.js'; +import { ILanguageConfigurationService } from '../../../common/languages/languageConfigurationRegistry.js'; +import { MonospaceLineBreaksComputerFactory } from '../../../common/viewModel/monospaceLineBreaksComputer.js'; +import { ViewModel } from '../../../common/viewModel/viewModelImpl.js'; +import { instantiateTextModel } from '../../../test/common/testTextModel.js'; +import { TestLanguageConfigurationService } from '../../common/modes/testLanguageConfigurationService.js'; +import { TestConfiguration } from '../config/testConfiguration.js'; +import { createCodeEditorServices } from '../testCodeEditor.js'; + +suite('ViewController - Bracket content selection', () => { + let disposables: DisposableStore; + let instantiationService: TestInstantiationService; + let languageConfigurationService: ILanguageConfigurationService; + let languageService: ILanguageService; + let viewModel: ViewModel | undefined; + + setup(() => { + disposables = new DisposableStore(); + instantiationService = createCodeEditorServices(disposables); + languageConfigurationService = instantiationService.get(ILanguageConfigurationService); + languageService = instantiationService.get(ILanguageService); + viewModel = undefined; + }); + + teardown(() => { + viewModel?.dispose(); + viewModel = undefined; + disposables.dispose(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + function createViewControllerWithText(text: string): ViewController { + const languageId = 'testMode'; + disposables.add(languageService.registerLanguage({ id: languageId })); + disposables.add(languageConfigurationService.register(languageId, { + brackets: [ + ['{', '}'], + ['[', ']'], + ['(', ')'], + ] + })); + + const configuration = disposables.add(new TestConfiguration({})); + const monospaceLineBreaksComputerFactory = MonospaceLineBreaksComputerFactory.create(configuration.options); + + viewModel = new ViewModel( + 1, // editorId + configuration, + disposables.add(instantiateTextModel(instantiationService, text, languageId)), + monospaceLineBreaksComputerFactory, + monospaceLineBreaksComputerFactory, + null!, + disposables.add(new TestLanguageConfigurationService()), + new TestThemeService(), + { setVisibleLines() { } }, + { batchChanges: (cb: any) => cb() } + ); + + return new ViewController( + configuration, + viewModel, + new ViewUserInputEvents(viewModel.coordinatesConverter), + { + paste: () => { }, + type: () => { }, + compositionType: () => { }, + startComposition: () => { }, + endComposition: () => { }, + cut: () => { } + } + ); + } + + function testBracketSelection(text: string, position: Position, expectedText: string | undefined) { + const controller = createViewControllerWithText(text); + controller.dispatchMouse({ + position, + mouseColumn: position.column, + startedOnLineNumbers: false, + revealType: NavigationCommandRevealType.Minimal, + mouseDownCount: 2, + inSelectionMode: false, + altKey: false, + ctrlKey: false, + metaKey: false, + shiftKey: false, + leftButton: true, + middleButton: false, + onInjectedText: false + }); + + const selections = viewModel!.getSelections(); + const selectedText = viewModel!.model.getValueInRange(selections[0]); + if (expectedText === undefined) { + assert.notStrictEqual(selectedText, expectedText); + } else { + assert.strictEqual(selectedText, expectedText); + } + } + + test('Select content after opening curly brace', () => { + testBracketSelection('var x = { hello };', new Position(1, 10), ' hello '); + }); + + test('Select content before closing curly brace', () => { + testBracketSelection('var x = { hello };', new Position(1, 17), ' hello '); + }); + + test('Select content after opening parenthesis', () => { + testBracketSelection('function foo(arg1, arg2) {}', new Position(1, 14), 'arg1, arg2'); + }); + + test('Select content before closing parenthesis', () => { + testBracketSelection('function foo(arg1, arg2) {}', new Position(1, 24), 'arg1, arg2'); + }); + + test('Select content after opening square bracket', () => { + testBracketSelection('const arr = [ 1, 2, 3 ];', new Position(1, 14), ' 1, 2, 3 '); + }); + + test('Select content before closing square bracket', () => { + testBracketSelection('const arr = [ 1, 2, 3 ];', new Position(1, 23), ' 1, 2, 3 '); + }); + + test('Select innermost bracket content with nested brackets', () => { + testBracketSelection('var x = { a: { b: 123 }};', new Position(1, 15), ' b: 123 '); + }); + + test('Empty brackets create empty selection', () => { + testBracketSelection('var x = {};', new Position(1, 10), ''); + }); +}); diff --git a/src/vs/editor/test/common/modes/supports/indentationRules.ts b/src/vs/editor/test/common/modes/supports/indentationRules.ts index 22a9d82b6b7d9..80fec490b9a62 100644 --- a/src/vs/editor/test/common/modes/supports/indentationRules.ts +++ b/src/vs/editor/test/common/modes/supports/indentationRules.ts @@ -44,7 +44,7 @@ export const luaIndentationRules = { export const vbIndentationRules = { // Decrease indent when line starts with End , Else, ElseIf, Case, Catch, Finally, Loop, Next, Wend, Until decreaseIndentPattern: /^\s*((End\s+(If|Sub|Function|Class|Module|Enum|Structure|Interface|Namespace|With|Select|Try|While|For|Property|Get|Set|SyncLock|Using|AddHandler|RaiseEvent|RemoveHandler|Event|Operator))|Else|ElseIf|Case|Catch|Finally|Loop|Next|Wend|Until)\b/i, - // Increase indent after lines ending with Then, or lines starting with If/While/For/Do/Select/Sub/Function/Class/etc (block-starting keywords) - // The pattern matches lines that start block structures but excludes lines that also end them (like single-line If...Then...End If) - increaseIndentPattern: /^\s*((If|ElseIf).*Then(?!\s+(End\s+If))\s*(('|REM).*)?$)|\b(Else|While|For|Do|Select\s+Case|Case|Sub|Function|Class|Module|Enum|Structure|Interface|Namespace|With|Try|Catch|Finally|SyncLock|Using|Property|Get|Set|AddHandler|RaiseEvent|RemoveHandler|Event|Operator)\b(?!.*\bEnd\s+(If|Sub|Function|Class|Module|Enum|Structure|Interface|Namespace|With|Select|Try|While|For|Property|Get|Set|SyncLock|Using|AddHandler|RaiseEvent|RemoveHandler|Event|Operator)\b).*(('|REM).*)?$/i, + // Increase indent after lines with block-starting keywords (Sub, Function, Class, Module, If...Then, etc.) + // Both alternatives are anchored to start of line with ^\s* + increaseIndentPattern: /^\s*((If|ElseIf).*Then(?!.*End\s+If)\s*(('|REM).*)?|(Else|While|For|Do|Select\s+Case|Case|Sub|Function|Class|Module|Enum|Structure|Interface|Namespace|With|Try|Catch|Finally|SyncLock|Using|Property|Get|Set|AddHandler|RaiseEvent|RemoveHandler|Event|Operator)\b(?!.*\bEnd\s+(If|Sub|Function|Class|Module|Enum|Structure|Interface|Namespace|With|Select|Try|While|For|Property|Get|Set|SyncLock|Using|AddHandler|RaiseEvent|RemoveHandler|Event|Operator)\b).*(('|REM).*)?)$/i, }; diff --git a/src/vs/editor/test/common/modes/supports/onEnterRules.ts b/src/vs/editor/test/common/modes/supports/onEnterRules.ts index b3cb35e27d511..8b52dd28c0107 100644 --- a/src/vs/editor/test/common/modes/supports/onEnterRules.ts +++ b/src/vs/editor/test/common/modes/supports/onEnterRules.ts @@ -132,6 +132,16 @@ export const htmlOnEnterRules = [ } ]; +export const vbOnEnterRules = [ + // Prevent indent after End statements and block terminators (but NOT ElseIf...Then or Else which should indent) + { + beforeText: /^\s*((End\s+(If|Sub|Function|Class|Module|Enum|Structure|Interface|Namespace|With|Select|Try|While|For|Property|Get|Set|SyncLock|Using|AddHandler|RaiseEvent|RemoveHandler|Event|Operator))|Loop|Next|Wend|Until)\b.*$/i, + action: { + indentAction: IndentAction.None + } + } +]; + /* export enum IndentAction { None = 0, diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index 501ada91cd95a..f21babec2fa0c 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -122,6 +122,7 @@ export const enum TerminalSettingId { FontLigaturesFallbackLigatures = 'terminal.integrated.fontLigatures.fallbackLigatures', EnableKittyKeyboardProtocol = 'terminal.integrated.enableKittyKeyboardProtocol', EnableWin32InputMode = 'terminal.integrated.enableWin32InputMode', + AllowInUntrustedWorkspace = 'terminal.integrated.allowInUntrustedWorkspace', // Developer/debug settings diff --git a/src/vs/platform/webWorker/browser/webWorkerServiceImpl.ts b/src/vs/platform/webWorker/browser/webWorkerServiceImpl.ts index 376e45857dbee..99238ddfbb51f 100644 --- a/src/vs/platform/webWorker/browser/webWorkerServiceImpl.ts +++ b/src/vs/platform/webWorker/browser/webWorkerServiceImpl.ts @@ -33,11 +33,15 @@ export class WebWorkerService implements IWebWorkerService { protected _createWorker(descriptor: WebWorkerDescriptor): Promise { const workerRunnerUrl = this.getWorkerUrl(descriptor); - const workerUrlWithNls = getWorkerBootstrapUrl(descriptor.label, workerRunnerUrl); + const workerUrlWithNls = getWorkerBootstrapUrl(descriptor.label, workerRunnerUrl, this._getWorkerLoadingFailedErrorMessage(descriptor)); const worker = new Worker(ttPolicy ? ttPolicy.createScriptURL(workerUrlWithNls) as unknown as string : workerUrlWithNls, { name: descriptor.label, type: 'module' }); return whenESMWorkerReady(worker); } + protected _getWorkerLoadingFailedErrorMessage(_descriptor: WebWorkerDescriptor): string | undefined { + return undefined; + } + getWorkerUrl(descriptor: WebWorkerDescriptor): string { if (!descriptor.esmModuleLocation) { throw new Error('Missing esmModuleLocation in WebWorkerDescriptor'); @@ -71,7 +75,7 @@ export function createBlobWorker(blobUrl: string, options?: WorkerOptions): Work return new Worker(ttPolicy ? ttPolicy.createScriptURL(blobUrl) as unknown as string : blobUrl, { ...options, type: 'module' }); } -function getWorkerBootstrapUrl(label: string, workerScriptUrl: string): string { +function getWorkerBootstrapUrl(label: string, workerScriptUrl: string, workerLoadingFailedErrorMessage: string | undefined): string { if (/^((http:)|(https:)|(file:))/.test(workerScriptUrl) && workerScriptUrl.substring(0, globalThis.origin.length) !== globalThis.origin) { // this is the cross-origin case // i.e. the webpage is running at a different origin than where the scripts are loaded from @@ -101,7 +105,11 @@ function getWorkerBootstrapUrl(label: string, workerScriptUrl: string): string { `globalThis._VSCODE_FILE_ROOT = ${JSON.stringify(globalThis._VSCODE_FILE_ROOT)};`, `const ttPolicy = globalThis.trustedTypes?.createPolicy('defaultWorkerFactory', { createScriptURL: value => value });`, `globalThis.workerttPolicy = ttPolicy;`, + + workerLoadingFailedErrorMessage ? 'try {' : '', `await import(ttPolicy?.createScriptURL(${JSON.stringify(workerScriptUrl)}) ?? ${JSON.stringify(workerScriptUrl)});`, + workerLoadingFailedErrorMessage ? `} catch (err) { console.error(${JSON.stringify(workerLoadingFailedErrorMessage)}, err); throw err; }` : '', + `globalThis.postMessage({ type: 'vscode-worker-ready' });`, `/*${label}*/` ]).join('')], { type: 'application/javascript' }); diff --git a/src/vs/workbench/api/common/extHostLanguageModels.ts b/src/vs/workbench/api/common/extHostLanguageModels.ts index dc5cc3d8bcac0..b82b1bd992a7b 100644 --- a/src/vs/workbench/api/common/extHostLanguageModels.ts +++ b/src/vs/workbench/api/common/extHostLanguageModels.ts @@ -27,6 +27,7 @@ import { IExtHostAuthentication } from './extHostAuthentication.js'; import { IExtHostRpcService } from './extHostRpcService.js'; import * as typeConvert from './extHostTypeConverters.js'; import * as extHostTypes from './extHostTypes.js'; +import { ChatAgentLocation } from '../../contrib/chat/common/constants.js'; export interface IExtHostLanguageModels extends ExtHostLanguageModels { } @@ -189,6 +190,22 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { checkProposedApiEnabled(data.extension, 'chatProvider'); } + const isDefaultForLocation: { [K in ChatAgentLocation]?: boolean } = {}; + if (isProposedApiEnabled(data.extension, 'chatProvider')) { + if (m.isDefault === true) { + for (const key of Object.values(ChatAgentLocation)) { + if (typeof key === 'string') { + isDefaultForLocation[key as ChatAgentLocation] = true; + } + } + } else if (typeof m.isDefault === 'object') { + for (const key of Object.keys(m.isDefault)) { + const enumKey = parseInt(key) as extHostTypes.ChatLocation; + isDefaultForLocation[typeConvert.ChatLocation.from(enumKey)] = m.isDefault[enumKey]; + } + } + } + return { metadata: { extension: data.extension.identifier, @@ -202,7 +219,7 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { maxInputTokens: m.maxInputTokens, maxOutputTokens: m.maxOutputTokens, auth, - isDefault: m.isDefault, + isDefaultForLocation, isUserSelectable: m.isUserSelectable, statusIcon: m.statusIcon, modelPickerCategory: m.category ?? DEFAULT_MODEL_PICKER_CATEGORY, @@ -333,7 +350,7 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { } for (const [modelIdentifier, modelData] of this._localModels) { - if (modelData.metadata.isDefault) { + if (modelData.metadata.isDefaultForLocation[ChatAgentLocation.Chat]) { defaultModelId = modelIdentifier; break; } diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupProviders.ts b/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupProviders.ts index 0c3e8b3a13410..37dbdf9e4080a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupProviders.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupProviders.ts @@ -400,7 +400,7 @@ export class SetupAgent extends Disposable implements IChatAgentImplementation { for (const id of languageModelsService.getLanguageModelIds()) { const model = languageModelsService.lookupLanguageModel(id); - if (model?.isDefault) { + if (model?.isDefaultForLocation[ChatAgentLocation.Chat]) { return true; } } diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index b5b4ce7d945bd..9669d88db0071 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -115,10 +115,12 @@ import { resizeImage } from '../../chatImageUtils.js'; import { IModelPickerDelegate, ModelPickerActionItem } from './modelPickerActionItem.js'; import { IModePickerDelegate, ModePickerActionItem } from './modePickerActionItem.js'; import { SearchableOptionPickerActionItem } from '../../chatSessions/searchableOptionPickerActionItemtest.js'; +import { mixin } from '../../../../../../base/common/objects.js'; const $ = dom.$; const INPUT_EDITOR_MAX_HEIGHT = 250; +const CachedLanguageModelsKey = 'chat.cachedLanguageModels.v2'; export interface IChatInputStyles { overlayBackground: string; @@ -152,7 +154,18 @@ const emptyInputState = observableMemento({ defaultValue: undefined, key: 'chat.untitledInputState', toStorage: JSON.stringify, - fromStorage: JSON.parse, + fromStorage(value) { + const obj = JSON.parse(value) as IChatModelInputState; + if (obj.selectedModel && !obj.selectedModel.metadata.isDefaultForLocation) { + // Migrate old `isDefault` to `isDefaultForLocation` + type OldILanguageModelChatMetadata = ILanguageModelChatMetadata & { isDefault?: boolean }; + const oldIsDefault = (obj.selectedModel.metadata as OldILanguageModelChatMetadata).isDefault; + const isDefaultForLocation = { [ChatAgentLocation.Chat]: Boolean(oldIsDefault) }; + mixin(obj.selectedModel.metadata, { isDefaultForLocation: isDefaultForLocation } satisfies Partial); + delete (obj.selectedModel.metadata as OldILanguageModelChatMetadata).isDefault; + } + return obj; + }, }); export class ChatInputPart extends Disposable implements IHistoryNavigationWidget { @@ -553,8 +566,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge // Remove vendor from cache since the models changed and what is stored is no longer valid // TODO @lramos15 - The cache should be less confusing since we have the LM Service cache + the view cache interacting weirdly this.storageService.store( - 'chat.cachedLanguageModels', - this.storageService.getObject('chat.cachedLanguageModels', StorageScope.APPLICATION, []).filter(m => !m.identifier.startsWith(vendor)), + CachedLanguageModelsKey, + this.storageService.getObject(CachedLanguageModelsKey, StorageScope.APPLICATION, []).filter(m => !m.identifier.startsWith(vendor)), StorageScope.APPLICATION, StorageTarget.MACHINE ); @@ -621,7 +634,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const model = this.getModels().find(m => m.identifier === persistedSelection); if (model) { // Only restore the model if it wasn't the default at the time of storing or it is now the default - if (!persistedAsDefault || model.metadata.isDefault) { + if (!persistedAsDefault || model.metadata.isDefaultForLocation[this.location]) { this.setCurrentLanguageModel(model); this.checkModelSupported(); } @@ -632,7 +645,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this._waitForPersistedLanguageModel.clear(); // Only restore the model if it wasn't the default at the time of storing or it is now the default - if (!persistedAsDefault || persistedModel.isDefault) { + if (!persistedAsDefault || persistedModel.isDefaultForLocation[this.location]) { if (persistedModel.isUserSelectable) { this.setCurrentLanguageModel({ metadata: persistedModel, identifier: persistedSelection }); this.checkModelSupported(); @@ -929,7 +942,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge // Store as global user preference (session-specific state is in the model's inputModel) this.storageService.store(this.getSelectedModelStorageKey(), model.identifier, StorageScope.APPLICATION, StorageTarget.USER); - this.storageService.store(this.getSelectedModelIsDefaultStorageKey(), !!model.metadata.isDefault, StorageScope.APPLICATION, StorageTarget.USER); + this.storageService.store(this.getSelectedModelIsDefaultStorageKey(), !!model.metadata.isDefaultForLocation[this.location], StorageScope.APPLICATION, StorageTarget.USER); this._onDidChangeCurrentLanguageModel.fire(model); @@ -986,13 +999,13 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } private getModels(): ILanguageModelChatMetadataAndIdentifier[] { - const cachedModels = this.storageService.getObject('chat.cachedLanguageModels', StorageScope.APPLICATION, []); + const cachedModels = this.storageService.getObject(CachedLanguageModelsKey, StorageScope.APPLICATION, []); let models = this.languageModelsService.getLanguageModelIds() .map(modelId => ({ identifier: modelId, metadata: this.languageModelsService.lookupLanguageModel(modelId)! })); - if (models.length === 0 || models.some(m => m.metadata.isDefault) === false) { + if (models.length === 0 || models.some(m => m.metadata.isDefaultForLocation[this.location]) === false) { models = cachedModels; } else { - this.storageService.store('chat.cachedLanguageModels', models, StorageScope.APPLICATION, StorageTarget.MACHINE); + this.storageService.store(CachedLanguageModelsKey, models, StorageScope.APPLICATION, StorageTarget.MACHINE); } models.sort((a, b) => a.metadata.name.localeCompare(b.metadata.name)); return models.filter(entry => entry.metadata?.isUserSelectable && this.modelSupportedForDefaultAgent(entry) && this.modelSupportedForInlineChat(entry)); @@ -1000,7 +1013,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private setCurrentLanguageModelToDefault() { const allModels = this.getModels(); - const defaultModel = allModels.find(m => m.metadata.isDefault) || allModels.find(m => m.metadata.isUserSelectable); + const defaultModel = allModels.find(m => m.metadata.isDefaultForLocation[this.location]) || allModels.find(m => m.metadata.isUserSelectable); if (defaultModel) { this.setCurrentLanguageModel(defaultModel); } diff --git a/src/vs/workbench/contrib/chat/common/languageModels.ts b/src/vs/workbench/contrib/chat/common/languageModels.ts index 40dd8f85483f6..3c92e9646f19d 100644 --- a/src/vs/workbench/contrib/chat/common/languageModels.ts +++ b/src/vs/workbench/contrib/chat/common/languageModels.ts @@ -28,10 +28,10 @@ import { ILogService } from '../../../../platform/log/common/log.js'; import { IQuickInputService, QuickInputHideReason } from '../../../../platform/quickinput/common/quickInput.js'; import { ISecretStorageService } from '../../../../platform/secrets/common/secrets.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; -import { ChatEntitlement, IChatEntitlementService } from '../../../services/chat/common/chatEntitlementService.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { ExtensionsRegistry } from '../../../services/extensions/common/extensionsRegistry.js'; import { ChatContextKeys } from './actions/chatContextKeys.js'; +import { ChatAgentLocation } from './constants.js'; import { ILanguageModelsProviderGroup, ILanguageModelsConfigurationService } from './languageModelsConfiguration.js'; export const enum ChatMessageRole { @@ -179,7 +179,7 @@ export interface ILanguageModelChatMetadata { readonly maxInputTokens: number; readonly maxOutputTokens: number; - readonly isDefault?: boolean; + readonly isDefaultForLocation: { [K in ChatAgentLocation]?: boolean }; readonly isUserSelectable?: boolean; readonly statusIcon?: ThemeIcon; readonly modelPickerCategory: { label: string; order: number } | undefined; @@ -429,7 +429,6 @@ export class LanguageModelsService implements ILanguageModelsService { @IStorageService private readonly _storageService: IStorageService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @IConfigurationService private readonly _configurationService: IConfigurationService, - @IChatEntitlementService private readonly _chatEntitlementService: IChatEntitlementService, @ILanguageModelsConfigurationService private readonly _languageModelsConfigurationService: ILanguageModelsConfigurationService, @IQuickInputService private readonly _quickInputService: IQuickInputService, @ISecretStorageService private readonly _secretStorageService: ISecretStorageService, @@ -437,16 +436,6 @@ export class LanguageModelsService implements ILanguageModelsService { this._hasUserSelectableModels = ChatContextKeys.languageModelsAreUserSelectable.bindTo(_contextKeyService); this._modelPickerUserPreferences = this._storageService.getObject>('chatModelPickerPreferences', StorageScope.PROFILE, this._modelPickerUserPreferences); - const entitlementChangeHandler = () => { - if ((this._chatEntitlementService.entitlement === ChatEntitlement.Business || this._chatEntitlementService.entitlement === ChatEntitlement.Enterprise) && !this._chatEntitlementService.isInternal) { - this._modelPickerUserPreferences = {}; - this._storageService.store('chatModelPickerPreferences', this._modelPickerUserPreferences, StorageScope.PROFILE, StorageTarget.USER); - } - }; - - entitlementChangeHandler(); - this._store.add(this._chatEntitlementService.onDidChangeEntitlement(entitlementChangeHandler)); - this._store.add(this.onDidChangeLanguageModels(() => this._hasUserSelectableModels.set(this._modelCache.size > 0 && Array.from(this._modelCache.values()).some(model => model.isUserSelectable)))); this._store.add(languageModelChatProviderExtensionPoint.setHandler((extensions) => { @@ -554,7 +543,7 @@ export class LanguageModelsService implements ILanguageModelsService { const languageModelsGroups: ILanguageModelsGroup[] = []; try { - const models = await this._resolveLanguageModels(vendorId, provider, { silent }); + const models = await this._resolveLanguageModels(provider, { silent }); if (models.length) { allModels.push(...models); languageModelsGroups.push({ models }); @@ -578,7 +567,7 @@ export class LanguageModelsService implements ILanguageModelsService { const configuration = await this._resolveConfiguration(group, vendor.configuration); try { - const models = await this._resolveLanguageModels(vendorId, provider, { group: group.name, silent, configuration }); + const models = await this._resolveLanguageModels(provider, { group: group.name, silent, configuration }); if (models.length) { allModels.push(...models); languageModelsGroups.push({ group, models }); @@ -598,27 +587,24 @@ export class LanguageModelsService implements ILanguageModelsService { this._modelsGroups.set(vendorId, languageModelsGroups); this._clearModelCache(vendorId); for (const model of allModels) { + if (this._modelCache.has(model.identifier)) { + this._logService.warn(`[LM] Model ${model.identifier} is already registered. Skipping.`); + continue; + } this._modelCache.set(model.identifier, model.metadata); } + this._logService.trace(`[LM] Resolved language models for vendor ${vendorId}`, allModels); this._onLanguageModelChange.fire(vendorId); }); } - private async _resolveLanguageModels(vendor: string, provider: ILanguageModelChatProvider, options: ILanguageModelChatInfoOptions): Promise { + private async _resolveLanguageModels(provider: ILanguageModelChatProvider, options: ILanguageModelChatInfoOptions): Promise { let models = await provider.provideLanguageModelChatInfo(options, CancellationToken.None); if (models.length) { // This is a bit of a hack, when prompting user if the provider returns any models that are user selectable then we only want to show those and not the entire model list if (!options.silent && models.some(m => m.metadata.isUserSelectable)) { models = models.filter(m => m.metadata.isUserSelectable || this._modelPickerUserPreferences[m.identifier] === true); } - - for (const { identifier } of models) { - if (this._modelCache.has(identifier)) { - this._logService.warn(`[LM] Model ${identifier} is already registered. Skipping.`); - continue; - } - } - this._logService.trace(`[LM] Resolved language models for vendor ${vendor}`, models); } return models; } diff --git a/src/vs/workbench/contrib/chat/test/browser/chatManagement/chatModelsViewModel.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatManagement/chatModelsViewModel.test.ts index 6b0adcdd5a5d4..87a4fc800800e 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatManagement/chatModelsViewModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatManagement/chatModelsViewModel.test.ts @@ -15,6 +15,7 @@ import { ExtensionIdentifier } from '../../../../../../platform/extensions/commo import { IStringDictionary } from '../../../../../../base/common/collections.js'; import { ILanguageModelsConfigurationService } from '../../../common/languageModelsConfiguration.js'; import { mock } from '../../../../../../base/test/common/mock.js'; +import { ChatAgentLocation } from '../../../common/constants.js'; class MockLanguageModelsService implements ILanguageModelsService { _serviceBrand: undefined; @@ -214,6 +215,9 @@ suite('ChatModelsViewModel', () => { toolCalling: true, vision: true, agentMode: false + }, + isDefaultForLocation: { + [ChatAgentLocation.Chat]: true } }); @@ -232,6 +236,9 @@ suite('ChatModelsViewModel', () => { toolCalling: true, vision: true, agentMode: true + }, + isDefaultForLocation: { + [ChatAgentLocation.Chat]: true } }); @@ -250,6 +257,9 @@ suite('ChatModelsViewModel', () => { toolCalling: true, vision: false, agentMode: false + }, + isDefaultForLocation: { + [ChatAgentLocation.Chat]: true } }); @@ -268,6 +278,9 @@ suite('ChatModelsViewModel', () => { toolCalling: false, vision: true, agentMode: false + }, + isDefaultForLocation: { + [ChatAgentLocation.Chat]: true } }); @@ -604,6 +617,9 @@ suite('ChatModelsViewModel', () => { toolCalling: true, vision: true, agentMode: false + }, + isDefaultForLocation: { + [ChatAgentLocation.Chat]: true } }); @@ -623,6 +639,9 @@ suite('ChatModelsViewModel', () => { toolCalling: true, vision: true, agentMode: true + }, + isDefaultForLocation: { + [ChatAgentLocation.Chat]: true } }); } @@ -708,6 +727,9 @@ suite('ChatModelsViewModel', () => { toolCalling: true, vision: false, agentMode: false + }, + isDefaultForLocation: { + [ChatAgentLocation.Chat]: true } }); @@ -734,6 +756,9 @@ suite('ChatModelsViewModel', () => { toolCalling: true, vision: false, agentMode: false + }, + isDefaultForLocation: { + [ChatAgentLocation.Chat]: true } }); @@ -843,6 +868,9 @@ suite('ChatModelsViewModel', () => { toolCalling: true, vision: false, agentMode: false + }, + isDefaultForLocation: { + [ChatAgentLocation.Chat]: true } }); diff --git a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHovers.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHovers.test.ts index fc20982332664..c4a7e74209f00 100644 --- a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHovers.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHovers.test.ts @@ -14,7 +14,7 @@ import { TestInstantiationService } from '../../../../../../../platform/instanti import { workbenchInstantiationService } from '../../../../../../test/browser/workbenchTestServices.js'; import { LanguageModelToolsService } from '../../../../browser/tools/languageModelToolsService.js'; import { ChatMode, CustomChatMode, IChatModeService } from '../../../../common/chatModes.js'; -import { ChatConfiguration } from '../../../../common/constants.js'; +import { ChatAgentLocation, ChatConfiguration } from '../../../../common/constants.js'; import { ILanguageModelToolsService, IToolData, ToolDataSource } from '../../../../common/tools/languageModelToolsService.js'; import { ILanguageModelChatMetadata, ILanguageModelsService } from '../../../../common/languageModels.js'; import { PromptHoverProvider } from '../../../../common/promptSyntax/languageProviders/promptHovers.js'; @@ -53,8 +53,8 @@ suite('PromptHoverProvider', () => { instaService.set(ILanguageModelToolsService, toolService); const testModels: ILanguageModelChatMetadata[] = [ - { id: 'mae-4', name: 'MAE 4', vendor: 'olama', version: '1.0', family: 'mae', modelPickerCategory: undefined, extension: new ExtensionIdentifier('a.b'), isUserSelectable: true, maxInputTokens: 8192, maxOutputTokens: 1024, capabilities: { agentMode: true, toolCalling: true } } satisfies ILanguageModelChatMetadata, - { id: 'mae-4.1', name: 'MAE 4.1', vendor: 'copilot', version: '1.0', family: 'mae', modelPickerCategory: undefined, extension: new ExtensionIdentifier('a.b'), isUserSelectable: true, maxInputTokens: 8192, maxOutputTokens: 1024, capabilities: { agentMode: true, toolCalling: true } } satisfies ILanguageModelChatMetadata, + { id: 'mae-4', name: 'MAE 4', vendor: 'olama', version: '1.0', family: 'mae', modelPickerCategory: undefined, extension: new ExtensionIdentifier('a.b'), isUserSelectable: true, maxInputTokens: 8192, maxOutputTokens: 1024, capabilities: { agentMode: true, toolCalling: true }, isDefaultForLocation: { [ChatAgentLocation.Chat]: true } } satisfies ILanguageModelChatMetadata, + { id: 'mae-4.1', name: 'MAE 4.1', vendor: 'copilot', version: '1.0', family: 'mae', modelPickerCategory: undefined, extension: new ExtensionIdentifier('a.b'), isUserSelectable: true, maxInputTokens: 8192, maxOutputTokens: 1024, capabilities: { agentMode: true, toolCalling: true }, isDefaultForLocation: { [ChatAgentLocation.Chat]: true } } satisfies ILanguageModelChatMetadata, ]; instaService.stub(ILanguageModelsService, { diff --git a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptValidator.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptValidator.test.ts index a59e2adf9acc3..fc3f3f9036319 100644 --- a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptValidator.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptValidator.test.ts @@ -18,7 +18,7 @@ import { IMarkerData, MarkerSeverity } from '../../../../../../../platform/marke import { workbenchInstantiationService } from '../../../../../../test/browser/workbenchTestServices.js'; import { LanguageModelToolsService } from '../../../../browser/tools/languageModelToolsService.js'; import { ChatMode, CustomChatMode, IChatModeService } from '../../../../common/chatModes.js'; -import { ChatConfiguration } from '../../../../common/constants.js'; +import { ChatAgentLocation, ChatConfiguration } from '../../../../common/constants.js'; import { ILanguageModelToolsService, IToolData, ToolDataSource } from '../../../../common/tools/languageModelToolsService.js'; import { ILanguageModelChatMetadata, ILanguageModelsService } from '../../../../common/languageModels.js'; import { getPromptFileExtension } from '../../../../common/promptSyntax/config/promptFileLocations.js'; @@ -112,9 +112,9 @@ suite('PromptValidator', () => { instaService.set(ILanguageModelToolsService, toolService); const testModels: ILanguageModelChatMetadata[] = [ - { id: 'mae-4', name: 'MAE 4', vendor: 'olama', version: '1.0', family: 'mae', modelPickerCategory: undefined, extension: new ExtensionIdentifier('a.b'), isUserSelectable: true, maxInputTokens: 8192, maxOutputTokens: 1024, capabilities: { agentMode: true, toolCalling: true } } satisfies ILanguageModelChatMetadata, - { id: 'mae-4.1', name: 'MAE 4.1', vendor: 'copilot', version: '1.0', family: 'mae', modelPickerCategory: undefined, extension: new ExtensionIdentifier('a.b'), isUserSelectable: true, maxInputTokens: 8192, maxOutputTokens: 1024, capabilities: { agentMode: true, toolCalling: true } } satisfies ILanguageModelChatMetadata, - { id: 'mae-3.5-turbo', name: 'MAE 3.5 Turbo', vendor: 'copilot', version: '1.0', family: 'mae', modelPickerCategory: undefined, extension: new ExtensionIdentifier('a.b'), isUserSelectable: true, maxInputTokens: 8192, maxOutputTokens: 1024 } satisfies ILanguageModelChatMetadata + { id: 'mae-4', name: 'MAE 4', vendor: 'olama', version: '1.0', family: 'mae', modelPickerCategory: undefined, extension: new ExtensionIdentifier('a.b'), isUserSelectable: true, maxInputTokens: 8192, maxOutputTokens: 1024, capabilities: { agentMode: true, toolCalling: true }, isDefaultForLocation: { [ChatAgentLocation.Chat]: true } } satisfies ILanguageModelChatMetadata, + { id: 'mae-4.1', name: 'MAE 4.1', vendor: 'copilot', version: '1.0', family: 'mae', modelPickerCategory: undefined, extension: new ExtensionIdentifier('a.b'), isUserSelectable: true, maxInputTokens: 8192, maxOutputTokens: 1024, capabilities: { agentMode: true, toolCalling: true }, isDefaultForLocation: { [ChatAgentLocation.Chat]: true } } satisfies ILanguageModelChatMetadata, + { id: 'mae-3.5-turbo', name: 'MAE 3.5 Turbo', vendor: 'copilot', version: '1.0', family: 'mae', modelPickerCategory: undefined, extension: new ExtensionIdentifier('a.b'), isUserSelectable: true, maxInputTokens: 8192, maxOutputTokens: 1024, isDefaultForLocation: { [ChatAgentLocation.Chat]: true } } satisfies ILanguageModelChatMetadata ]; instaService.stub(ILanguageModelsService, { diff --git a/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts b/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts index b0c19555dff9d..3bc0df9fce853 100644 --- a/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts @@ -10,12 +10,12 @@ import { DisposableStore } from '../../../../../base/common/lifecycle.js'; import { mock } from '../../../../../base/test/common/mock.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { NullLogService } from '../../../../../platform/log/common/log.js'; -import { ChatMessageRole, languageModelChatProviderExtensionPoint, LanguageModelsService, IChatMessage, IChatResponsePart } from '../../common/languageModels.js'; +import { ChatMessageRole, languageModelChatProviderExtensionPoint, LanguageModelsService, IChatMessage, IChatResponsePart, ILanguageModelChatMetadata } from '../../common/languageModels.js'; import { IExtensionService, nullExtensionDescription } from '../../../../services/extensions/common/extensions.js'; import { ExtensionsRegistry } from '../../../../services/extensions/common/extensionsRegistry.js'; import { DEFAULT_MODEL_PICKER_CATEGORY } from '../../common/widget/input/modelPickerWidget.js'; import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js'; -import { TestChatEntitlementService, TestStorageService } from '../../../../test/common/workbenchTestServices.js'; +import { TestStorageService } from '../../../../test/common/workbenchTestServices.js'; import { Event } from '../../../../../base/common/event.js'; import { MockContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js'; import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; @@ -44,7 +44,6 @@ suite('LanguageModels', function () { new TestStorageService(), new MockContextKeyService(), new TestConfigurationService(), - new TestChatEntitlementService(), new class extends mock() { override getLanguageModelsProviderGroups() { return []; @@ -80,7 +79,8 @@ suite('LanguageModels', function () { id: 'test-id-1', maxInputTokens: 100, maxOutputTokens: 100, - }, + isDefaultForLocation: {} + } satisfies ILanguageModelChatMetadata, { extension: nullExtensionDescription.identifier, name: 'Pretty Name', @@ -91,7 +91,8 @@ suite('LanguageModels', function () { id: 'test-id-12', maxInputTokens: 100, maxOutputTokens: 100, - } + isDefaultForLocation: {} + } satisfies ILanguageModelChatMetadata ]; const modelMetadataAndIdentifier = modelMetadata.map(m => ({ metadata: m, @@ -154,7 +155,8 @@ suite('LanguageModels', function () { maxInputTokens: 100, maxOutputTokens: 100, modelPickerCategory: DEFAULT_MODEL_PICKER_CATEGORY, - } + isDefaultForLocation: {} + } satisfies ILanguageModelChatMetadata ]; const modelMetadataAndIdentifier = modelMetadata.map(m => ({ metadata: m, @@ -256,7 +258,6 @@ suite('LanguageModels - When Clause', function () { new TestStorageService(), contextKeyService, new TestConfigurationService(), - new TestChatEntitlementService(), new class extends mock() { }, new class extends mock() { }, new TestSecretStorageService(), diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 3b27190d3ca8b..05f5ee69e13da 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -205,7 +205,7 @@ export class InlineChatController implements IEditorContribution { this._store.add(result); this._store.add(result.widget.chatWidget.input.onDidChangeCurrentLanguageModel(newModel => { - InlineChatController._selectVendorDefaultLanguageModel = Boolean(newModel.metadata.isDefault); + InlineChatController._selectVendorDefaultLanguageModel = Boolean(newModel.metadata.isDefaultForLocation[location.location]); })); result.domNode.classList.add('inline-chat-2'); @@ -440,11 +440,11 @@ export class InlineChatController implements IEditorContribution { // or unless the user has chosen to persist their model choice const persistModelChoice = this._configurationService.getValue(InlineChatConfigKeys.PersistModelChoice); const model = this._zone.value.widget.chatWidget.input.selectedLanguageModel; - if (!persistModelChoice && InlineChatController._selectVendorDefaultLanguageModel && model && !model.metadata.isDefault) { + if (!persistModelChoice && InlineChatController._selectVendorDefaultLanguageModel && model && !model.metadata.isDefaultForLocation[session.chatModel.initialLocation]) { const ids = await this._languageModelService.selectLanguageModels({ vendor: model.metadata.vendor }, false); for (const identifier of ids) { const candidate = this._languageModelService.lookupLanguageModel(identifier); - if (candidate?.isDefault) { + if (candidate?.isDefaultForLocation[session.chatModel.initialLocation]) { this._zone.value.widget.chatWidget.input.setCurrentLanguageModel({ metadata: candidate, identifier }); break; } diff --git a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index 83a96e87b0599..c1979e024c534 100644 --- a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -57,7 +57,10 @@ Registry.as(Extensions.Configuration).registerConfigurat [InlineChatConfigKeys.PersistModelChoice]: { description: localize('persistModelChoice', "Whether to persist the selected language model choice across inline chat sessions. The default is not to persist and to use the vendor's default model for inline chat because that yields the best experience."), default: false, - type: 'boolean' + type: 'boolean', + experiment: { + mode: 'auto' + } } } }); diff --git a/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts b/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts index 936baf661039a..7678a38062e6b 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts @@ -52,7 +52,7 @@ import { CHAT_CONFIG_MENU_ID } from '../../chat/browser/actions/chatActions.js'; import { ChatViewId, IChatWidgetService } from '../../chat/browser/chat.js'; import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js'; import { IChatElicitationRequest, IChatToolInvocation } from '../../chat/common/chatService/chatService.js'; -import { ChatModeKind } from '../../chat/common/constants.js'; +import { ChatAgentLocation, ChatModeKind } from '../../chat/common/constants.js'; import { ILanguageModelsService } from '../../chat/common/languageModels.js'; import { ILanguageModelToolsService } from '../../chat/common/tools/languageModelToolsService.js'; import { VIEW_CONTAINER } from '../../extensions/browser/extensions.contribution.js'; @@ -1067,7 +1067,7 @@ export class McpConfigureSamplingModels extends Action2 { label: model.name, description: model.tooltip, id, - picked: existingIds.size ? existingIds.has(id) : model.isDefault, + picked: existingIds.size ? existingIds.has(id) : model.isDefaultForLocation[ChatAgentLocation.Chat], }; }).filter(isDefined); diff --git a/src/vs/workbench/contrib/mcp/common/mcpSamplingService.ts b/src/vs/workbench/contrib/mcp/common/mcpSamplingService.ts index 1438caa3827f3..d19658136fb7e 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpSamplingService.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpSamplingService.ts @@ -18,6 +18,7 @@ import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; +import { ChatAgentLocation } from '../../chat/common/constants.js'; import { ChatImageMimeType, ChatMessageRole, IChatMessage, IChatMessagePart, ILanguageModelsService } from '../../chat/common/languageModels.js'; import { McpCommandIds } from './mcpCommandIds.js'; import { IMcpServerSamplingConfiguration, mcpServerSamplingSection } from './mcpConfiguration.js'; @@ -232,7 +233,7 @@ export class McpSamplingService extends Disposable implements IMcpSamplingServic } // 2. Get the configured models, or the default model(s) - const foundModelIdsDeep = config.allowedModels?.filter(m => !!this._languageModelsService.lookupLanguageModel(m)) || this._languageModelsService.getLanguageModelIds().filter(m => this._languageModelsService.lookupLanguageModel(m)?.isDefault); + const foundModelIdsDeep = config.allowedModels?.filter(m => !!this._languageModelsService.lookupLanguageModel(m)) || this._languageModelsService.getLanguageModelIds().filter(m => this._languageModelsService.lookupLanguageModel(m)?.isDefaultForLocation[ChatAgentLocation.Chat]); const foundModelIds = foundModelIdsDeep.flat().sort((a, b) => b.length - a.length); // Sort by length to prefer most specific diff --git a/src/vs/workbench/contrib/output/browser/outputView.ts b/src/vs/workbench/contrib/output/browser/outputView.ts index ed28ae7b65418..c3b2b8c245b29 100644 --- a/src/vs/workbench/contrib/output/browser/outputView.ts +++ b/src/vs/workbench/contrib/output/browser/outputView.ts @@ -268,6 +268,7 @@ export class OutputEditor extends AbstractTextResourceEditor { options.padding = undefined; options.readOnly = true; options.domReadOnly = true; + options.roundedSelection = false; options.unicodeHighlight = { nonBasicASCII: false, invisibleCharacters: false, diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index ce934e637ac01..03975fd67f7fb 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -1867,10 +1867,14 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } private async _trust(): Promise { - return (await this._workspaceTrustRequestService.requestWorkspaceTrust( - { - message: nls.localize('terminal.requestTrust', "Creating a terminal process requires executing code") - })) === true; + if (this._configurationService.getValue(TerminalSettingId.AllowInUntrustedWorkspace)) { + this._logService.info(`Workspace trust check bypassed due to ${TerminalSettingId.AllowInUntrustedWorkspace}`); + return true; + } + const trustRequest = await this._workspaceTrustRequestService.requestWorkspaceTrust({ + message: nls.localize('terminal.requestTrust', "Creating a terminal process requires executing code") + }); + return trustRequest === true; } @debounce(2000) diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index 14cfcf5eff1cd..d1dc45e4dfa32 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -672,6 +672,12 @@ const terminalConfiguration: IStringDictionary = { localize('terminal.integrated.focusAfterRun.none', "Do nothing."), ] }, + [TerminalSettingId.AllowInUntrustedWorkspace]: { + restricted: true, + markdownDescription: localize('terminal.integrated.allowInUntrustedWorkspace', "Controls whether terminals can be created in an untrusted workspace.\n\n**This feature bypasses a security protection that prevents terminals from launching in untrusted workspaces. The reason this is a security risk is because shells are often set up to potentially execute code automatically based on the contents of the current working directory. This should be safe to use provided your shell is set up in such a way that code execution in the folder never happens.**"), + type: 'boolean', + default: false + }, [TerminalSettingId.DeveloperPtyHostLatency]: { description: localize('terminal.integrated.developer.ptyHost.latency', "Simulated latency in milliseconds applied to all calls made to the pty host. This is useful for testing terminal behavior under high latency conditions."), type: 'number', diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts index 86229a4238036..e9e1adddbefec 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts @@ -477,7 +477,7 @@ export const terminalChatAgentToolsConfiguration: IStringDictionary; constructor( + private readonly defaultAccountConfig: IDefaultAccountConfig, @IDefaultAccountService private readonly defaultAccountService: IDefaultAccountService, @IConfigurationService private readonly configurationService: IConfigurationService, @IAuthenticationService private readonly authenticationService: IAuthenticationService, + @IAuthenticationExtensionsService private readonly authenticationExtensionsService: IAuthenticationExtensionsService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IExtensionService private readonly extensionService: IExtensionService, - @IProductService private readonly productService: IProductService, @IRequestService private readonly requestService: IRequestService, @ILogService private readonly logService: ILogService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @@ -123,35 +125,9 @@ export class DefaultAccountManagementContribution extends Disposable implements ) { super(); this.accountStatusContext = CONTEXT_DEFAULT_ACCOUNT_STATE.bindTo(contextKeyService); - this.initialize().then(() => { - type DefaultAccountStatusTelemetry = { - status: string; - initial: boolean; - }; - type DefaultAccountStatusTelemetryClassification = { - owner: 'sandy081'; - comment: 'Log default account availability status'; - status: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Indicates whether default account is available or not.' }; - initial: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Indicates whether this is the initial status report.' }; - }; - this.telemetryService.publicLog2('defaultaccount:status', { status: this.defaultAccount ? 'available' : 'unavailable', initial: true }); - - this._register(this.authenticationService.onDidChangeSessions(async e => { - if (e.providerId !== this.getDefaultAccountProviderId()) { - return; - } - if (this.defaultAccount && e.event.removed?.some(session => session.id === this.defaultAccount?.sessionId)) { - this.setDefaultAccount(null); - } else { - this.setDefaultAccount(await this.getDefaultAccountFromAuthenticatedSessions(e.providerId, this.productService.defaultAccount!.authenticationProvider.scopes)); - } - - this.telemetryService.publicLog2('defaultaccount:status', { status: this.defaultAccount ? 'available' : 'unavailable', initial: false }); - })); - }); } - private async initialize(): Promise { + async setup(): Promise { this.logService.debug('[DefaultAccount] Starting initialization'); let defaultAccount: IDefaultAccount | null = null; try { @@ -159,16 +135,46 @@ export class DefaultAccountManagementContribution extends Disposable implements } catch (error) { this.logService.error('[DefaultAccount] Error during initialization', getErrorMessage(error)); } + this.setDefaultAccount(defaultAccount); this.logService.debug('[DefaultAccount] Initialization complete'); + + type DefaultAccountStatusTelemetry = { + status: string; + initial: boolean; + }; + type DefaultAccountStatusTelemetryClassification = { + owner: 'sandy081'; + comment: 'Log default account availability status'; + status: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Indicates whether default account is available or not.' }; + initial: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Indicates whether this is the initial status report.' }; + }; + this.telemetryService.publicLog2('defaultaccount:status', { status: this.defaultAccount ? 'available' : 'unavailable', initial: true }); + + this._register(this.defaultAccountService.onDidChangeDefaultAccount(account => { + this.telemetryService.publicLog2('defaultaccount:status', { status: account ? 'available' : 'unavailable', initial: false }); + })); + + this._register(this.authenticationService.onDidChangeSessions(async e => { + if (e.providerId !== this.getDefaultAccountProviderId()) { + return; + } + if (this.defaultAccount && e.event.removed?.some(session => session.id === this.defaultAccount?.sessionId)) { + this.setDefaultAccount(null); + } else { + this.setDefaultAccount(await this.getDefaultAccountFromAuthenticatedSessions(e.providerId, this.defaultAccountConfig.authenticationProvider.scopes)); + } + })); + + this._register(this.authenticationExtensionsService.onDidChangeAccountPreference(async e => { + if (e.providerId !== this.getDefaultAccountProviderId()) { + return; + } + this.setDefaultAccount(await this.getDefaultAccountFromAuthenticatedSessions(e.providerId, this.defaultAccountConfig.authenticationProvider.scopes)); + })); } private async fetchDefaultAccount(): Promise { - if (!this.productService.defaultAccount) { - this.logService.debug('[DefaultAccount] No default account configuration in product service, skipping initialization'); - return null; - } - if (isWeb && !this.environmentService.remoteAuthority) { this.logService.debug('[DefaultAccount] Running in web without remote, skipping initialization'); return null; @@ -189,8 +195,8 @@ export class DefaultAccountManagementContribution extends Disposable implements return null; } - this.registerSignInAction(defaultAccountProviderId, this.productService.defaultAccount.authenticationProvider.scopes[0]); - return await this.getDefaultAccountFromAuthenticatedSessions(defaultAccountProviderId, this.productService.defaultAccount.authenticationProvider.scopes); + this.registerSignInAction(this.defaultAccountConfig.authenticationProvider.scopes[0]); + return await this.getDefaultAccountFromAuthenticatedSessions(defaultAccountProviderId, this.defaultAccountConfig.authenticationProvider.scopes); } private setDefaultAccount(account: IDefaultAccount | null): void { @@ -266,7 +272,22 @@ export class DefaultAccountManagementContribution extends Disposable implements private async getSessions(authProviderId: string): Promise { for (let attempt = 1; attempt <= 3; attempt++) { try { - return await this.authenticationService.getSessions(authProviderId, undefined, undefined, true); + let preferredAccount: AuthenticationSessionAccount | undefined; + let preferredAccountName: string | undefined; + for (const preferredExtension of this.defaultAccountConfig.preferredExtensions) { + preferredAccountName = this.authenticationExtensionsService.getAccountPreference(preferredExtension, authProviderId); + if (preferredAccountName) { + break; + } + } + for (const account of await this.authenticationService.getAccounts(authProviderId)) { + if (account.label === preferredAccountName) { + preferredAccount = account; + break; + } + } + + return await this.authenticationService.getSessions(authProviderId, undefined, { account: preferredAccount }, true); } catch (error) { this.logService.warn(`[DefaultAccount] Attempt ${attempt} to get sessions failed:`, getErrorMessage(error)); if (attempt === 3) { @@ -378,10 +399,6 @@ export class DefaultAccountManagementContribution extends Disposable implements } private getChatEntitlementUrl(): string | undefined { - if (!this.productService.defaultAccount) { - return undefined; - } - if (this.isEnterpriseAuthenticationProvider(this.getDefaultAccountProviderId())) { try { const enterpriseUrl = this.getEnterpriseUrl(); @@ -394,14 +411,10 @@ export class DefaultAccountManagementContribution extends Disposable implements } } - return this.productService.defaultAccount?.chatEntitlementUrl; + return this.defaultAccountConfig.chatEntitlementUrl; } private getTokenEntitlementUrl(): string | undefined { - if (!this.productService.defaultAccount) { - return undefined; - } - if (this.isEnterpriseAuthenticationProvider(this.getDefaultAccountProviderId())) { try { const enterpriseUrl = this.getEnterpriseUrl(); @@ -414,14 +427,10 @@ export class DefaultAccountManagementContribution extends Disposable implements } } - return this.productService.defaultAccount?.tokenEntitlementUrl; + return this.defaultAccountConfig.tokenEntitlementUrl; } private getMcpRegistryDataUrl(): string | undefined { - if (!this.productService.defaultAccount) { - return undefined; - } - if (this.isEnterpriseAuthenticationProvider(this.getDefaultAccountProviderId())) { try { const enterpriseUrl = this.getEnterpriseUrl(); @@ -434,36 +443,29 @@ export class DefaultAccountManagementContribution extends Disposable implements } } - return this.productService.defaultAccount?.mcpRegistryDataUrl; + return this.defaultAccountConfig.mcpRegistryDataUrl; } - private getDefaultAccountProviderId(): string | undefined { - if (this.productService.defaultAccount && this.configurationService.getValue(this.productService.defaultAccount.authenticationProvider.enterpriseProviderConfig) === this.productService.defaultAccount?.authenticationProvider.enterpriseProviderId) { - return this.productService.defaultAccount?.authenticationProvider.enterpriseProviderId; + private getDefaultAccountProviderId(): string { + if (this.configurationService.getValue(this.defaultAccountConfig.authenticationProvider.enterpriseProviderConfig) === this.defaultAccountConfig?.authenticationProvider.enterpriseProviderId) { + return this.defaultAccountConfig.authenticationProvider.enterpriseProviderId; } - return this.productService.defaultAccount?.authenticationProvider.id; + return this.defaultAccountConfig.authenticationProvider.id; } - private isEnterpriseAuthenticationProvider(providerId: string | undefined): boolean { - if (!providerId) { - return false; - } - - return providerId === this.productService.defaultAccount?.authenticationProvider.enterpriseProviderId; + private isEnterpriseAuthenticationProvider(providerId: string): boolean { + return providerId === this.defaultAccountConfig.authenticationProvider.enterpriseProviderId; } private getEnterpriseUrl(): URL | undefined { - if (!this.productService.defaultAccount) { - return undefined; - } - const value = this.configurationService.getValue(this.productService.defaultAccount.authenticationProvider.enterpriseProviderUriSetting); + const value = this.configurationService.getValue(this.defaultAccountConfig.authenticationProvider.enterpriseProviderUriSetting); if (!isString(value)) { return undefined; } return new URL(value); } - private registerSignInAction(authProviderId: string, scopes: string[]): void { + private registerSignInAction(defaultAccountScopes: string[]): void { const that = this; this._register(registerAction2(class extends Action2 { constructor() { @@ -472,12 +474,39 @@ export class DefaultAccountManagementContribution extends Disposable implements title: localize('sign in', "Sign in"), }); } - run(): Promise { - return that.authenticationService.createSession(authProviderId, scopes); + async run(accessor: ServicesAccessor, options?: { additionalScopes?: readonly string[];[key: string]: unknown }): Promise { + const authProviderId = that.getDefaultAccountProviderId(); + if (!authProviderId) { + throw new Error('No default account provider configured'); + } + const { additionalScopes, ...sessionOptions } = options ?? {}; + const scopes = additionalScopes ? distinct([...defaultAccountScopes, ...additionalScopes]) : defaultAccountScopes; + const session = await that.authenticationService.createSession(authProviderId, scopes, sessionOptions); + for (const preferredExtension of that.defaultAccountConfig.preferredExtensions) { + that.authenticationExtensionsService.updateAccountPreference(preferredExtension, authProviderId, session.account); + } } })); } } -registerWorkbenchContribution2('workbench.contributions.defaultAccountManagement', DefaultAccountManagementContribution, WorkbenchPhase.AfterRestored); +class DefaultAccountSetupContribution extends Disposable implements IWorkbenchContribution { + + static ID = 'workbench.contributions.defaultAccountSetup'; + + constructor( + @IProductService productService: IProductService, + @IInstantiationService instantiationService: IInstantiationService, + @ILogService logService: ILogService, + ) { + super(); + if (productService.defaultAccount) { + this._register(instantiationService.createInstance(DefaultAccountSetup, productService.defaultAccount)).setup(); + } else { + logService.debug('[DefaultAccount] No default account configuration in product service, skipping initialization'); + } + } +} + +registerWorkbenchContribution2('workbench.contributions.defaultAccountManagement', DefaultAccountSetupContribution, WorkbenchPhase.AfterRestored); diff --git a/src/vscode-dts/vscode.proposed.chatProvider.d.ts b/src/vscode-dts/vscode.proposed.chatProvider.d.ts index 755325b964c21..4879dbb93d915 100644 --- a/src/vscode-dts/vscode.proposed.chatProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatProvider.d.ts @@ -36,7 +36,7 @@ declare module 'vscode' { * Whether or not this will be selected by default in the model picker * NOT BEING FINALIZED */ - readonly isDefault?: boolean; + readonly isDefault?: boolean | { [K in ChatLocation]?: boolean }; /** * Whether or not the model will show up in the model picker immediately upon being made known via {@linkcode LanguageModelChatProvider.provideLanguageModelChatInformation}.