Merge branch 'main' into betterFailureMessage

This commit is contained in:
Aaron Munger 2023-03-01 15:39:07 -08:00 committed by GitHub
commit d989227967
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 3178 additions and 13 deletions

View file

@ -154,6 +154,10 @@
"name": "vs/workbench/contrib/notebook",
"project": "vscode-workbench"
},
{
"name": "vs/workbench/contrib/interactiveSession",
"project": "vscode-workbench"
},
{
"name": "vs/workbench/contrib/quickaccess",
"project": "vscode-workbench"

View file

@ -33,6 +33,7 @@ export interface MarkedOptions extends marked.MarkedOptions {
export interface MarkdownRenderOptions extends FormattedTextRenderOptions {
readonly codeBlockRenderer?: (languageId: string, value: string) => Promise<HTMLElement>;
readonly asyncRenderCallback?: () => void;
readonly fillInIncompleteTokens?: boolean;
}
const defaultMarkedRenderers = Object.freeze({
@ -228,7 +229,19 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
value = markdownEscapeEscapedIcons(value);
}
let renderedMarkdown = marked.parse(value, markedOptions);
let renderedMarkdown: string;
if (options.fillInIncompleteTokens) {
// The defaults are applied by parse but not lexer()/parser(), and they need to be present
const opts = {
...marked.defaults,
...markedOptions
};
const tokens = marked.lexer(value, opts);
const newTokens = fillInIncompleteTokens(tokens);
renderedMarkdown = marked.parser(newTokens, opts);
} else {
renderedMarkdown = marked.parse(value, markedOptions);
}
// Rewrite theme icons
if (markdown.supportThemeIcons) {
@ -518,3 +531,85 @@ const plainTextRenderer = new Lazy<marked.Renderer>(() => {
};
return renderer;
});
function mergeRawTokenText(tokens: marked.Token[]): string {
let mergedTokenText = '';
tokens.forEach(token => {
mergedTokenText += token.raw;
});
return mergedTokenText;
}
export function fillInIncompleteTokens(tokens: marked.TokensList): marked.TokensList {
let i: number;
let newTokens: marked.Token[] | undefined;
for (i = 0; i < tokens.length; i++) {
const token = tokens[i];
if (token.type === 'paragraph' && token.raw.match(/(\n|^)```/)) {
// If the code block was complete, it would be in a type='code'
newTokens = completeCodeBlock(tokens.slice(i));
break;
}
if (token.type === 'paragraph' && token.raw.match(/(\n|^)\|/)) {
newTokens = completeTable(tokens.slice(i));
break;
}
}
if (newTokens) {
const newTokensList = [
...tokens.slice(0, i),
...newTokens,
];
(newTokensList as marked.TokensList).links = tokens.links;
return newTokensList as marked.TokensList;
}
return tokens;
}
function completeCodeBlock(tokens: marked.Token[]): marked.Token[] {
const mergedRawText = mergeRawTokenText(tokens);
return marked.lexer(mergedRawText + '\n```');
}
function completeTable(tokens: marked.Token[]): marked.Token[] {
const mergedRawText = mergeRawTokenText(tokens);
const lines = mergedRawText.split('\n');
let numCols: number | undefined; // The number of line1 col headers
let hasSeparatorRow = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (typeof numCols === 'undefined' && line.match(/^\s*\|/)) {
const line1Matches = line.match(/(\|[^\|]+)(?=\||$)/g);
if (line1Matches) {
numCols = line1Matches.length;
}
} else if (typeof numCols === 'number') {
if (line.match(/^\s*\|/)) {
if (i !== lines.length - 1) {
// We got the line1 header row, and the line2 separator row, but there are more lines, and it wasn't parsed as a table!
// That's strange and means that the table is probably malformed in the source, so I won't try to patch it up.
return tokens;
}
// Got a line2 separator row- partial or complete, doesn't matter, we'll replace it with a correct one
hasSeparatorRow = true;
} else {
// The line after the header row isn't a valid separator row, so the table is malformed, don't fix it up
return tokens;
}
}
}
if (typeof numCols === 'number' && numCols > 0) {
const prefixText = hasSeparatorRow ? lines.slice(0, -1).join('\n') : mergedRawText;
const line1EndsInPipe = !!prefixText.match(/\|\s*$/);
const newRawText = prefixText + (line1EndsInPipe ? '' : '|') + `\n|${' --- |'.repeat(numCols)}`;
return marked.lexer(newRawText);
}
return tokens;
}

View file

@ -4,8 +4,9 @@
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { renderMarkdown, renderMarkdownAsPlaintext } from 'vs/base/browser/markdownRenderer';
import { fillInIncompleteTokens, renderMarkdown, renderMarkdownAsPlaintext } from 'vs/base/browser/markdownRenderer';
import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent';
import { marked } from 'vs/base/common/marked/marked';
import { parse } from 'vs/base/common/marshalling';
import { isWeb } from 'vs/base/common/platform';
import { URI } from 'vs/base/common/uri';
@ -325,4 +326,185 @@ suite('MarkdownRenderer', () => {
assert.strictEqual(result.innerHTML, `<img src="vscode-file://vscode-app/images/cat.gif">`);
});
});
suite('fillInIncompleteTokens', () => {
function ignoreRaw(...tokenLists: marked.Token[][]): void {
tokenLists.forEach(tokens => {
tokens.forEach(t => t.raw = '');
});
}
const completeTable = '| a | b |\n| --- | --- |';
test('complete table', () => {
const tokens = marked.lexer(completeTable);
const newTokens = fillInIncompleteTokens(tokens);
assert.equal(newTokens, tokens);
});
test('full header only', () => {
const incompleteTable = '| a | b |';
const tokens = marked.lexer(incompleteTable);
const completeTableTokens = marked.lexer(completeTable);
const newTokens = fillInIncompleteTokens(tokens);
assert.deepStrictEqual(newTokens, completeTableTokens);
});
test('full header only with trailing space', () => {
const incompleteTable = '| a | b | ';
const tokens = marked.lexer(incompleteTable);
const completeTableTokens = marked.lexer(completeTable);
const newTokens = fillInIncompleteTokens(tokens);
ignoreRaw(newTokens, completeTableTokens);
assert.deepStrictEqual(newTokens, completeTableTokens);
});
test('incomplete header', () => {
const incompleteTable = '| a | b';
const tokens = marked.lexer(incompleteTable);
const completeTableTokens = marked.lexer(completeTable);
const newTokens = fillInIncompleteTokens(tokens);
ignoreRaw(newTokens, completeTableTokens);
assert.deepStrictEqual(newTokens, completeTableTokens);
});
test('incomplete header one column', () => {
const incompleteTable = '| a ';
const tokens = marked.lexer(incompleteTable);
const completeTableTokens = marked.lexer(incompleteTable + '|\n| --- |');
const newTokens = fillInIncompleteTokens(tokens);
ignoreRaw(newTokens, completeTableTokens);
assert.deepStrictEqual(newTokens, completeTableTokens);
});
test('full header with extras', () => {
const incompleteTable = '| a **bold** | b _italics_ |';
const tokens = marked.lexer(incompleteTable);
const completeTableTokens = marked.lexer(incompleteTable + '\n| --- | --- |');
const newTokens = fillInIncompleteTokens(tokens);
assert.deepStrictEqual(newTokens, completeTableTokens);
});
test('full header with leading text', () => {
// Parsing this gives one token and one 'text' subtoken
const incompleteTable = 'here is a table\n| a | b |';
const tokens = marked.lexer(incompleteTable);
const completeTableTokens = marked.lexer(incompleteTable + '\n| --- | --- |');
const newTokens = fillInIncompleteTokens(tokens);
assert.deepStrictEqual(newTokens, completeTableTokens);
});
test('full header with leading other stuff', () => {
// Parsing this gives one token and one 'text' subtoken
const incompleteTable = '```js\nconst xyz = 123;\n```\n| a | b |';
const tokens = marked.lexer(incompleteTable);
const completeTableTokens = marked.lexer(incompleteTable + '\n| --- | --- |');
const newTokens = fillInIncompleteTokens(tokens);
assert.deepStrictEqual(newTokens, completeTableTokens);
});
test('full header with incomplete separator', () => {
const incompleteTable = '| a | b |\n| ---';
const tokens = marked.lexer(incompleteTable);
const completeTableTokens = marked.lexer(completeTable);
const newTokens = fillInIncompleteTokens(tokens);
assert.deepStrictEqual(newTokens, completeTableTokens);
});
test('full header with incomplete separator 2', () => {
const incompleteTable = '| a | b |\n| --- |';
const tokens = marked.lexer(incompleteTable);
const completeTableTokens = marked.lexer(completeTable);
const newTokens = fillInIncompleteTokens(tokens);
assert.deepStrictEqual(newTokens, completeTableTokens);
});
test('full header with incomplete separator 3', () => {
const incompleteTable = '| a | b |\n|';
const tokens = marked.lexer(incompleteTable);
const completeTableTokens = marked.lexer(completeTable);
const newTokens = fillInIncompleteTokens(tokens);
assert.deepStrictEqual(newTokens, completeTableTokens);
});
test('not a table', () => {
const incompleteTable = '| a | b |\nsome text';
const tokens = marked.lexer(incompleteTable);
const newTokens = fillInIncompleteTokens(tokens);
assert.deepStrictEqual(newTokens, tokens);
});
test('not a table 2', () => {
const incompleteTable = '| a | b |\n| --- |\nsome text';
const tokens = marked.lexer(incompleteTable);
const newTokens = fillInIncompleteTokens(tokens);
assert.deepStrictEqual(newTokens, tokens);
});
test('complete code block', () => {
const completeCodeblock = '```js\nconst xyz = 123;\n```';
const tokens = marked.lexer(completeCodeblock);
const newTokens = fillInIncompleteTokens(tokens);
assert.equal(newTokens, tokens);
});
test('code block header only', () => {
const incompleteCodeblock = '```js';
const tokens = marked.lexer(incompleteCodeblock);
const newTokens = fillInIncompleteTokens(tokens);
const completeCodeblockTokens = marked.lexer(incompleteCodeblock + '\n```');
assert.deepStrictEqual(newTokens, completeCodeblockTokens);
});
test('code block header no lang', () => {
const incompleteCodeblock = '```';
const tokens = marked.lexer(incompleteCodeblock);
const newTokens = fillInIncompleteTokens(tokens);
const completeCodeblockTokens = marked.lexer(incompleteCodeblock + '\n```');
assert.deepStrictEqual(newTokens, completeCodeblockTokens);
});
test('code block header and some code', () => {
const incompleteCodeblock = '```js\nconst';
const tokens = marked.lexer(incompleteCodeblock);
const newTokens = fillInIncompleteTokens(tokens);
const completeCodeblockTokens = marked.lexer(incompleteCodeblock + '\n```');
assert.deepStrictEqual(newTokens, completeCodeblockTokens);
});
test('code block header with leading text', () => {
const incompleteCodeblock = 'some text\n```js';
const tokens = marked.lexer(incompleteCodeblock);
const newTokens = fillInIncompleteTokens(tokens);
const completeCodeblockTokens = marked.lexer(incompleteCodeblock + '\n```');
assert.deepStrictEqual(newTokens, completeCodeblockTokens);
});
test('code block header with leading text and some code', () => {
const incompleteCodeblock = 'some text\n```js\nconst';
const tokens = marked.lexer(incompleteCodeblock);
const newTokens = fillInIncompleteTokens(tokens);
const completeCodeblockTokens = marked.lexer(incompleteCodeblock + '\n```');
assert.deepStrictEqual(newTokens, completeCodeblockTokens);
});
});
});

View file

@ -22,7 +22,7 @@ import { DocumentRangeSemanticTokensProvider } from 'vs/editor/common/languages'
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
import { ISemanticTokensStylingService } from 'vs/editor/common/services/semanticTokensStyling';
class ViewportSemanticTokensContribution extends Disposable implements IEditorContribution {
export class ViewportSemanticTokensContribution extends Disposable implements IEditorContribution {
public static readonly ID = 'editor.contrib.viewportSemanticTokens';

View file

@ -51,7 +51,7 @@ class SelectionRanges {
}
}
class SmartSelectController implements IEditorContribution {
export class SmartSelectController implements IEditorContribution {
static readonly ID = 'editor.contrib.smartSelectController';

View file

@ -70,6 +70,7 @@ import './mainThreadNotebookKernels';
import './mainThreadNotebookDocumentsAndEditors';
import './mainThreadNotebookRenderers';
import './mainThreadInteractive';
import './mainThreadInteractiveSession';
import './mainThreadTask';
import './mainThreadLabelService';
import './mainThreadTunnelService';

View file

@ -0,0 +1,102 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { DisposableMap } from 'vs/base/common/lifecycle';
import { ExtHostContext, ExtHostInteractiveSessionShape, IInteractiveRequestDto, MainContext, MainThreadInteractiveSessionShape } from 'vs/workbench/api/common/extHost.protocol';
import { IInteractiveSessionContributionService } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionContributionService';
import { IInteractiveProgress, IInteractiveRequest, IInteractiveResponse, IInteractiveSessionService } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionService';
import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers';
@extHostNamedCustomer(MainContext.MainThreadInteractiveSession)
export class MainThreadInteractiveSession implements MainThreadInteractiveSessionShape {
private readonly _inputRegistrations = new DisposableMap<number>();
private readonly _registrations = new DisposableMap<number>();
private readonly _activeRequestProgressCallbacks = new Map<string, (progress: IInteractiveProgress) => void>();
private readonly _proxy: ExtHostInteractiveSessionShape;
constructor(
extHostContext: IExtHostContext,
@IInteractiveSessionService private readonly _interactiveSessionService: IInteractiveSessionService,
@IInteractiveSessionContributionService private readonly interactiveSessionContribService: IInteractiveSessionContributionService
) {
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostInteractiveSession);
}
dispose(): void {
this._inputRegistrations.dispose();
this._registrations.dispose();
}
async $registerInteractiveSessionProvider(handle: number, id: string): Promise<void> {
if (!this.interactiveSessionContribService.registeredProviders.find(staticProvider => staticProvider.id === id)) {
throw new Error(`Provider ${id} must be declared in the package.json.`);
}
const unreg = this._interactiveSessionService.registerProvider({
id,
prepareSession: async (initialState, token) => {
const session = await this._proxy.$prepareInteractiveSession(handle, initialState, token);
if (!session) {
return undefined;
}
return {
...session,
dispose: () => {
this._proxy.$releaseSession(session.id);
}
};
},
resolveRequest: async (session, context, token) => {
const dto = await this._proxy.$resolveInteractiveRequest(handle, session.id, context, token);
return <IInteractiveRequest>{
session,
...dto
};
},
provideReply: async (request, progress, token) => {
const id = `${handle}_${request.session.id}`;
this._activeRequestProgressCallbacks.set(id, progress);
try {
const requestDto: IInteractiveRequestDto = {
message: request.message,
};
const dto = await this._proxy.$provideInteractiveReply(handle, request.session.id, requestDto, token);
return <IInteractiveResponse>{
session: request.session,
...dto
};
} finally {
this._activeRequestProgressCallbacks.delete(id);
}
},
provideSuggestions: (token) => {
return this._proxy.$provideInitialSuggestions(handle, token);
}
});
this._registrations.set(handle, unreg);
}
$acceptInteractiveResponseProgress(handle: number, sessionId: number, progress: IInteractiveProgress): void {
const id = `${handle}_${sessionId}`;
this._activeRequestProgressCallbacks.get(id)?.(progress);
}
async $acceptInteractiveSessionState(sessionId: number, state: any): Promise<void> {
this._interactiveSessionService.acceptNewSessionState(sessionId, state);
}
$addInteractiveSessionRequest(context: any): void {
this._interactiveSessionService.addInteractiveRequest(context);
}
async $unregisterInteractiveSessionProvider(handle: number): Promise<void> {
this._registrations.deleteAndDispose(handle);
}
}

View file

@ -97,6 +97,7 @@ import { IExtHostLocalizationService } from 'vs/workbench/api/common/extHostLoca
import { EditSessionIdentityMatch } from 'vs/platform/workspace/common/editSessions';
import { ExtHostProfileContentHandlers } from 'vs/workbench/api/common/extHostProfileContentHandler';
import { ExtHostQuickDiff } from 'vs/workbench/api/common/extHostQuickDiff';
import { ExtHostInteractiveSession } from 'vs/workbench/api/common/extHostInteractiveSession';
export interface IExtensionRegistries {
mine: ExtensionDescriptionRegistry;
@ -191,6 +192,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
const extHostUriOpeners = rpcProtocol.set(ExtHostContext.ExtHostUriOpeners, new ExtHostUriOpeners(rpcProtocol));
const extHostProfileContentHandlers = rpcProtocol.set(ExtHostContext.ExtHostProfileContentHandlers, new ExtHostProfileContentHandlers(rpcProtocol));
rpcProtocol.set(ExtHostContext.ExtHostInteractive, new ExtHostInteractive(rpcProtocol, extHostNotebook, extHostDocumentsAndEditors, extHostCommands, extHostLogService));
const extHostInteractiveSession = rpcProtocol.set(ExtHostContext.ExtHostInteractiveSession, new ExtHostInteractiveSession(rpcProtocol, extHostLogService));
// Check that no named customers are missing
const expected = Object.values<ProxyIdentifier<any>>(ExtHostContext);
@ -1208,6 +1210,22 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
}
};
// namespace: interactive
const interactive: typeof vscode.interactive = {
// IMPORTANT
// this needs to be updated whenever the API proposal changes
_version: 1,
registerInteractiveSessionProvider(id: string, provider: vscode.InteractiveSessionProvider) {
checkProposedApiEnabled(extension, 'interactive');
return extHostInteractiveSession.registerInteractiveSessionProvider(extension, id, provider);
},
addInteractiveRequest(context: vscode.InteractiveSessionRequestArgs) {
checkProposedApiEnabled(extension, 'interactive');
return extHostInteractiveSession.addInteractiveSessionRequest(context);
}
};
return <typeof vscode>{
version: initData.version,
// namespaces
@ -1217,6 +1235,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
debug,
env,
extensions,
interactive,
l10n,
languages,
notebooks,

View file

@ -12,6 +12,7 @@ import { IMarkdownString } from 'vs/base/common/htmlContent';
import { IDisposable } from 'vs/base/common/lifecycle';
import * as performance from 'vs/base/common/performance';
import Severity from 'vs/base/common/severity';
import { ThemeColor, ThemeIcon } from 'vs/base/common/themables';
import { URI, UriComponents, UriDto } from 'vs/base/common/uri';
import { RenderLineNumbersType, TextEditorCursorStyle } from 'vs/editor/common/config/editorOptions';
import { ISingleEditOperation } from 'vs/editor/common/core/editOperation';
@ -20,8 +21,8 @@ import { IRange } from 'vs/editor/common/core/range';
import { ISelection, Selection } from 'vs/editor/common/core/selection';
import { IChange } from 'vs/editor/common/diff/smartLinesDiffComputer';
import * as editorCommon from 'vs/editor/common/editorCommon';
import * as languages from 'vs/editor/common/languages';
import { StandardTokenType } from 'vs/editor/common/encodedTokenAttributes';
import * as languages from 'vs/editor/common/languages';
import { CharacterPair, CommentRule, EnterAction } from 'vs/editor/common/languages/languageConfiguration';
import { EndOfLineSequence } from 'vs/editor/common/model';
import { IModelChangedEvent } from 'vs/editor/common/model/mirrorTextModel';
@ -39,9 +40,10 @@ import * as quickInput from 'vs/platform/quickinput/common/quickInput';
import { IRemoteConnectionData, TunnelDescription } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { ClassifiedEvent, IGDPRProperty, OmitMetadata, StrictPropertyCheck } from 'vs/platform/telemetry/common/gdprTypings';
import { TelemetryLevel } from 'vs/platform/telemetry/common/telemetry';
import { ISerializableEnvironmentVariableCollection } from 'vs/platform/terminal/common/environmentVariable';
import { ICreateContributedTerminalProfileOptions, IProcessProperty, IShellLaunchConfigDto, ITerminalEnvironment, ITerminalLaunchError, ITerminalProfile, TerminalExitReason, TerminalLocation } from 'vs/platform/terminal/common/terminal';
import { ThemeIcon, ThemeColor } from 'vs/base/common/themables';
import { ProvidedPortAttributes, TunnelCreationOptions, TunnelOptions, TunnelPrivacyId, TunnelProviderFeatures } from 'vs/platform/tunnel/common/tunnel';
import { EditSessionIdentityMatch } from 'vs/platform/workspace/common/editSessions';
import { WorkspaceTrustRequestOptions } from 'vs/platform/workspace/common/workspaceTrust';
import * as tasks from 'vs/workbench/api/common/shared/tasks';
import { SaveReason } from 'vs/workbench/common/editor';
@ -52,11 +54,9 @@ import * as notebookCommon from 'vs/workbench/contrib/notebook/common/notebookCo
import { CellExecutionUpdateType } from 'vs/workbench/contrib/notebook/common/notebookExecutionService';
import { ICellExecutionComplete, ICellExecutionStateUpdate } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService';
import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange';
import { OutputChannelUpdateMode } from 'vs/workbench/services/output/common/output';
import { InputValidationType } from 'vs/workbench/contrib/scm/common/scm';
import { IWorkspaceSymbol } from 'vs/workbench/contrib/search/common/search';
import { ISerializableEnvironmentVariableCollection } from 'vs/platform/terminal/common/environmentVariable';
import { CoverageDetails, ExtensionRunTestsRequest, ICallProfileRunHandler, IFileCoverage, ISerializedTestResults, ITestItem, ITestMessage, ITestRunProfile, ITestRunTask, ResolvedTestRunRequest, IStartControllerTests, TestResultState, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testTypes';
import { CoverageDetails, ExtensionRunTestsRequest, ICallProfileRunHandler, IFileCoverage, ISerializedTestResults, IStartControllerTests, ITestItem, ITestMessage, ITestRunProfile, ITestRunTask, ResolvedTestRunRequest, TestResultState, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testTypes';
import { Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor } from 'vs/workbench/contrib/timeline/common/timeline';
import { TypeHierarchyItem } from 'vs/workbench/contrib/typeHierarchy/common/typeHierarchy';
import { AuthenticationProviderInformation, AuthenticationSession, AuthenticationSessionsChangeEvent } from 'vs/workbench/services/authentication/common/authentication';
@ -64,14 +64,14 @@ import { EditorGroupColumn } from 'vs/workbench/services/editor/common/editorGro
import { IExtensionDescriptionDelta, IStaticWorkspaceData } from 'vs/workbench/services/extensions/common/extensionHostProtocol';
import { IResolveAuthorityResult } from 'vs/workbench/services/extensions/common/extensionHostProxy';
import { ActivationKind, ExtensionActivationReason, MissingExtensionDependency } from 'vs/workbench/services/extensions/common/extensions';
import { createProxyIdentifier, Dto, IRPCProtocol, SerializableObjectWithBuffers } from 'vs/workbench/services/extensions/common/proxyIdentifier';
import { Dto, IRPCProtocol, SerializableObjectWithBuffers, createProxyIdentifier } from 'vs/workbench/services/extensions/common/proxyIdentifier';
import { ILanguageStatus } from 'vs/workbench/services/languageStatus/common/languageStatusService';
import { OutputChannelUpdateMode } from 'vs/workbench/services/output/common/output';
import { CandidatePort } from 'vs/workbench/services/remote/common/remoteExplorerService';
import { ITextQueryBuilderOptions } from 'vs/workbench/services/search/common/queryBuilder';
import * as search from 'vs/workbench/services/search/common/search';
import { EditSessionIdentityMatch } from 'vs/platform/workspace/common/editSessions';
import { TerminalCommandMatchResult, TerminalQuickFixCommand, TerminalQuickFixOpener } from 'vscode';
import { ISaveProfileResult } from 'vs/workbench/services/userDataProfile/common/userDataProfile';
import { TerminalCommandMatchResult, TerminalQuickFixCommand, TerminalQuickFixOpener } from 'vscode';
export type TerminalQuickFix = TerminalQuickFixCommand | TerminalQuickFixOpener;
@ -1074,6 +1074,38 @@ export interface MainThreadUrlsShape extends IDisposable {
$createAppUri(uri: UriComponents): Promise<UriComponents>;
}
export interface IInteractiveSessionDto {
id: number;
}
export interface IInteractiveRequestDto {
message: string;
}
export interface IInteractiveResponseDto {
followups?: string[];
}
export interface IInteractiveResponseProgressDto {
responsePart: string;
}
export interface MainThreadInteractiveSessionShape extends IDisposable {
$registerInteractiveSessionProvider(handle: number, id: string): Promise<void>;
$acceptInteractiveSessionState(sessionId: number, state: any): Promise<void>;
$addInteractiveSessionRequest(context: any): void;
$unregisterInteractiveSessionProvider(handle: number): Promise<void>;
$acceptInteractiveResponseProgress(handle: number, sessionId: number, progress: IInteractiveResponseProgressDto): void;
}
export interface ExtHostInteractiveSessionShape {
$prepareInteractiveSession(handle: number, initialState: any, token: CancellationToken): Promise<IInteractiveSessionDto | undefined>;
$resolveInteractiveRequest(handle: number, sessionId: number, context: any, token: CancellationToken): Promise<IInteractiveRequestDto | undefined>;
$provideInitialSuggestions(handle: number, token: CancellationToken): Promise<string[] | undefined>;
$provideInteractiveReply(handle: number, sessionid: number, request: IInteractiveRequestDto, token: CancellationToken): Promise<IInteractiveResponseDto | undefined>;
$releaseSession(sessionId: number): void;
}
export interface ExtHostUrlsShape {
$handleExternalUri(handle: number, uri: UriComponents): Promise<void>;
}
@ -2378,6 +2410,7 @@ export const MainContext = {
MainThreadNotebookKernels: createProxyIdentifier<MainThreadNotebookKernelsShape>('MainThreadNotebookKernels'),
MainThreadNotebookRenderers: createProxyIdentifier<MainThreadNotebookRenderersShape>('MainThreadNotebookRenderers'),
MainThreadInteractive: createProxyIdentifier<MainThreadInteractiveShape>('MainThreadInteractive'),
MainThreadInteractiveSession: createProxyIdentifier<MainThreadInteractiveSessionShape>('MainThreadInteractiveSession'),
MainThreadTheming: createProxyIdentifier<MainThreadThemingShape>('MainThreadTheming'),
MainThreadTunnelService: createProxyIdentifier<MainThreadTunnelServiceShape>('MainThreadTunnelService'),
MainThreadTimeline: createProxyIdentifier<MainThreadTimelineShape>('MainThreadTimeline'),
@ -2433,6 +2466,7 @@ export const ExtHostContext = {
ExtHostNotebookKernels: createProxyIdentifier<ExtHostNotebookKernelsShape>('ExtHostNotebookKernels'),
ExtHostNotebookRenderers: createProxyIdentifier<ExtHostNotebookRenderersShape>('ExtHostNotebookRenderers'),
ExtHostInteractive: createProxyIdentifier<ExtHostInteractiveShape>('ExtHostInteractive'),
ExtHostInteractiveSession: createProxyIdentifier<ExtHostInteractiveSessionShape>('ExtHostInteractiveSession'),
ExtHostTheming: createProxyIdentifier<ExtHostThemingShape>('ExtHostTheming'),
ExtHostTunnelService: createProxyIdentifier<ExtHostTunnelServiceShape>('ExtHostTunnelService'),
ExtHostAuthentication: createProxyIdentifier<ExtHostAuthenticationShape>('ExtHostAuthentication'),

View file

@ -0,0 +1,165 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CancellationToken } from 'vs/base/common/cancellation';
import { toDisposable } from 'vs/base/common/lifecycle';
import { withNullAsUndefined } from 'vs/base/common/types';
import { IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { ILogService } from 'vs/platform/log/common/log';
import { ExtHostInteractiveSessionShape, IInteractiveRequestDto, IInteractiveResponseDto, IInteractiveSessionDto, IMainContext, MainContext, MainThreadInteractiveSessionShape } from 'vs/workbench/api/common/extHost.protocol';
import type * as vscode from 'vscode';
class InteractiveSessionProviderWrapper {
private static _pool = 0;
readonly handle: number = InteractiveSessionProviderWrapper._pool++;
constructor(
readonly extension: Readonly<IRelaxedExtensionDescription>,
readonly provider: vscode.InteractiveSessionProvider,
) { }
}
export class ExtHostInteractiveSession implements ExtHostInteractiveSessionShape {
private static _nextId = 0;
private readonly _interactiveSessionProvider = new Map<number, InteractiveSessionProviderWrapper>();
private readonly _interactiveSessions = new Map<number, vscode.InteractiveSession>();
private readonly _proxy: MainThreadInteractiveSessionShape;
constructor(
mainContext: IMainContext,
_logService: ILogService
) {
this._proxy = mainContext.getProxy(MainContext.MainThreadInteractiveSession);
}
//#region interactive session
registerInteractiveSessionProvider(extension: Readonly<IRelaxedExtensionDescription>, id: string, provider: vscode.InteractiveSessionProvider): vscode.Disposable {
const wrapper = new InteractiveSessionProviderWrapper(extension, provider);
this._interactiveSessionProvider.set(wrapper.handle, wrapper);
this._proxy.$registerInteractiveSessionProvider(wrapper.handle, id);
return toDisposable(() => {
this._proxy.$unregisterInteractiveSessionProvider(wrapper.handle);
this._interactiveSessionProvider.delete(wrapper.handle);
});
}
addInteractiveSessionRequest(context: vscode.InteractiveSessionRequestArgs): void {
this._proxy.$addInteractiveSessionRequest(context);
}
async $prepareInteractiveSession(handle: number, initialState: any, token: CancellationToken): Promise<IInteractiveSessionDto | undefined> {
const entry = this._interactiveSessionProvider.get(handle);
if (!entry) {
return undefined;
}
const session = await entry.provider.prepareSession(initialState, token);
if (!session) {
return undefined;
}
const id = ExtHostInteractiveSession._nextId++;
this._interactiveSessions.set(id, session);
return { id };
}
async $resolveInteractiveRequest(handle: number, sessionId: number, context: any, token: CancellationToken): Promise<IInteractiveRequestDto | undefined> {
const entry = this._interactiveSessionProvider.get(handle);
if (!entry) {
return undefined;
}
const realSession = this._interactiveSessions.get(sessionId);
if (!realSession) {
return undefined;
}
if (!entry.provider.resolveRequest) {
return undefined;
}
const request = await entry.provider.resolveRequest(realSession, context, token);
if (request) {
return {
message: request.message,
};
}
return undefined;
}
async $provideInitialSuggestions(handle: number, token: CancellationToken): Promise<string[] | undefined> {
const entry = this._interactiveSessionProvider.get(handle);
if (!entry) {
return undefined;
}
if (!entry.provider.provideInitialSuggestions) {
return undefined;
}
return withNullAsUndefined(await entry.provider.provideInitialSuggestions(token));
}
async $provideInteractiveReply(handle: number, sessionId: number, request: IInteractiveRequestDto, token: CancellationToken): Promise<IInteractiveResponseDto | undefined> {
const entry = this._interactiveSessionProvider.get(handle);
if (!entry) {
return undefined;
}
const realSession = this._interactiveSessions.get(sessionId);
if (!realSession) {
return;
}
const requestObj: vscode.InteractiveRequest = {
session: realSession,
message: request.message,
};
if (entry.provider.provideResponse) {
const res = await entry.provider.provideResponse(requestObj, token);
if (realSession.saveState) {
const newState = realSession.saveState();
this._proxy.$acceptInteractiveSessionState(sessionId, newState);
}
if (!res) {
return;
}
this._proxy.$acceptInteractiveResponseProgress(handle, sessionId, { responsePart: res.content });
return { followups: res.followups };
} else if (entry.provider.provideResponseWithProgress) {
const progressObj: vscode.Progress<vscode.InteractiveProgress> = {
report: (progress: vscode.InteractiveProgress) => this._proxy.$acceptInteractiveResponseProgress(handle, sessionId, { responsePart: progress.content })
};
const res = await entry.provider.provideResponseWithProgress(requestObj, progressObj, token);
if (realSession.saveState) {
const newState = realSession.saveState();
this._proxy.$acceptInteractiveSessionState(sessionId, newState);
}
if (!res) {
return;
}
return { followups: res.followups };
}
throw new Error('provider must implement either provideResponse or provideResponseWithProgress');
}
$releaseSession(sessionId: number) {
this._interactiveSessions.delete(sessionId);
}
//#endregion
}

View file

@ -0,0 +1,104 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable } from 'vs/base/common/lifecycle';
import { isMacintosh } from 'vs/base/common/platform';
import * as nls from 'vs/nls';
import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { Registry } from 'vs/platform/registry/common/platform';
import { EditorPaneDescriptor, IEditorPaneRegistry } from 'vs/workbench/browser/editor';
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions';
import { EditorExtensions } from 'vs/workbench/common/editor';
import { registerInteractiveSessionActions } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionActions';
import { InteractiveSessionContributionService } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionContributionServiceImpl';
import { InteractiveSessionEditor } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionEditor';
import { InteractiveSessionEditorInput } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionEditorInput';
import { IInteractiveSessionContributionService } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionContributionService';
import { IInteractiveSessionService } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionService';
import { IEditorResolverService, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService';
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
import '../common/interactiveSessionColors';
import { InteractiveSessionService } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionServiceImpl';
// Register configuration
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
configurationRegistry.registerConfiguration({
id: 'interactiveSessionSidebar',
title: nls.localize('interactiveSessionConfigurationTitle', "Interactive Session"),
type: 'object',
properties: {
'interactiveSession.editor.fontSize': {
type: 'number',
description: nls.localize('interactiveSession.editor.fontSize', "Controls the font size in pixels in the Interactive Session Sidebar."),
default: isMacintosh ? 12 : 14,
},
'interactiveSession.editor.fontFamily': {
type: 'string',
description: nls.localize('interactiveSession.editor.fontFamily', "Controls the font family in the Interactive Session Sidebar."),
default: 'default'
},
'interactiveSession.editor.fontWeight': {
type: 'string',
description: nls.localize('interactiveSession.editor.fontWeight', "Controls the font weight in the Interactive Session Sidebar."),
default: 'default'
},
'interactiveSession.editor.lineHeight': {
type: 'number',
description: nls.localize('interactiveSession.editor.lineHeight', "Controls the line height in pixels in the Interactive Session Sidebar. Use 0 to compute the line height from the font size."),
default: 0
}
}
});
Registry.as<IEditorPaneRegistry>(EditorExtensions.EditorPane).registerEditorPane(
EditorPaneDescriptor.create(
InteractiveSessionEditor,
InteractiveSessionEditor.ID,
nls.localize('interactiveSession', "Interactive Session")
),
[
new SyncDescriptor(InteractiveSessionEditorInput)
]
);
class InteractiveSessionResolverContribution extends Disposable {
constructor(
@IEditorResolverService editorResolverService: IEditorResolverService,
@IInstantiationService instantiationService: IInstantiationService,
) {
super();
this._register(editorResolverService.registerEditor(
`${InteractiveSessionEditor.SCHEME}:**/**`,
{
id: InteractiveSessionEditor.ID,
label: nls.localize('interactiveSession', "Interactive Session"),
priority: RegisteredEditorPriority.builtin
},
{
singlePerResource: true,
canSupportResource: resource => resource.scheme === InteractiveSessionEditor.SCHEME
},
{
createEditorInput: ({ resource, options }) => {
return { editor: instantiationService.createInstance(InteractiveSessionEditorInput, resource), options };
}
}
));
}
}
const workbenchContributionsRegistry = Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench);
workbenchContributionsRegistry.registerWorkbenchContribution(InteractiveSessionResolverContribution, LifecyclePhase.Starting);
registerInteractiveSessionActions();
registerSingleton(IInteractiveSessionService, InteractiveSessionService, InstantiationType.Delayed);
registerSingleton(IInteractiveSessionContributionService, InteractiveSessionContributionService, InstantiationType.Eager);

View file

@ -0,0 +1,183 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Codicon } from 'vs/base/common/codicons';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { EditorAction, ServicesAccessor, registerEditorAction } from 'vs/editor/browser/editorExtensions';
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { localize } from 'vs/nls';
import { Action2, IAction2Options, MenuId, registerAction2 } from 'vs/platform/actions/common/actions';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { ActiveEditorContext } from 'vs/workbench/common/contextkeys';
import { IViewsService } from 'vs/workbench/common/views';
import { IInteractiveSessionEditorOptions, InteractiveSessionEditor } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionEditor';
import { InteractiveSessionViewPane } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionSidebar';
import { CONTEXT_IN_INTERACTIVE_INPUT, CONTEXT_IN_INTERACTIVE_SESSION, InteractiveSessionWidget } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionWidget';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
const category = { value: localize('interactiveSession.category', "Interactive Session"), original: 'Interactive Session' };
export const ClearInteractiveSessionActionDescriptor: Readonly<IAction2Options> = {
id: 'workbench.action.interactiveSession.clear',
title: {
value: localize('interactiveSession.clear.label', "Clear"),
original: 'Clear'
},
category,
icon: Codicon.clearAll,
f1: false
};
export function registerInteractiveSessionActions() {
registerEditorAction(class InteractiveSessionAcceptInput extends EditorAction {
constructor() {
super({
id: 'interactiveSession.action.acceptInput',
label: localize({ key: 'actions.ineractiveSession.acceptInput', comment: ['Apply input from the interactive session input box'] }, "Interactive Session Accept Input"),
alias: 'Interactive Session Accept Input',
precondition: CONTEXT_IN_INTERACTIVE_INPUT,
kbOpts: {
kbExpr: EditorContextKeys.textInputFocus,
primary: KeyCode.Enter,
weight: KeybindingWeight.EditorContrib
}
});
}
run(_accessor: ServicesAccessor, editor: ICodeEditor): void | Promise<void> {
const editorUri = editor.getModel()?.uri;
if (editorUri) {
InteractiveSessionWidget.getViewByInputUri(editorUri)?.acceptInput();
}
}
});
// registerAction2(class OpenInteractiveSessionWindow extends Action2 {
// constructor() {
// super({
// id: 'workbench.action.interactiveSession.start',
// title: localize('interactiveSession', 'Open Interactive Session...'),
// icon: Codicon.commentDiscussion,
// precondition: ContextKeyExpr.and(CTX_INTERACTIVE_EDITOR_VISIBLE),
// f1: false,
// menu: {
// id: MENU_INTERACTIVE_EDITOR_WIDGET,
// group: 'Z',
// order: 1
// }
// });
// }
// override run(accessor: ServicesAccessor, ...args: any[]): void {
// const viewsService = accessor.get(IViewsService);
// viewsService.openView(InteractiveSessionViewPane.ID, true);
// }
// });
registerAction2(class ClearEditorAction extends Action2 {
constructor() {
super({
id: 'workbench.action.interactiveSessionEditor.clear',
title: {
value: localize('interactiveSession.clear.label', "Clear"),
original: 'Clear'
},
icon: Codicon.clearAll,
f1: false,
menu: [{
id: MenuId.EditorTitle,
group: 'navigation',
order: 0,
when: ActiveEditorContext.isEqualTo(InteractiveSessionEditor.ID),
}]
});
}
run(accessor: ServicesAccessor, ...args: any[]) {
const editorService = accessor.get(IEditorService);
if (editorService.activeEditorPane instanceof InteractiveSessionEditor) {
editorService.activeEditorPane.clear();
}
}
});
registerEditorAction(class FocusInteractiveSessionAction extends EditorAction {
constructor() {
super({
id: 'interactiveSession.action.focus',
label: localize('actions.interactiveSession.focus', "Focus Interactive Session"),
alias: 'Focus Interactive Session',
precondition: CONTEXT_IN_INTERACTIVE_INPUT,
kbOpts: {
kbExpr: EditorContextKeys.textInputFocus,
primary: KeyMod.CtrlCmd | KeyCode.UpArrow,
weight: KeybindingWeight.EditorContrib
}
});
}
run(_accessor: ServicesAccessor, editor: ICodeEditor): void | Promise<void> {
const editorUri = editor.getModel()?.uri;
if (editorUri) {
InteractiveSessionWidget.getViewByInputUri(editorUri)?.focusLastMessage();
}
}
});
registerAction2(class FocusInteractiveSessionInputAction extends Action2 {
constructor() {
super({
id: 'workbench.action.interactiveSession.focusInput',
title: {
value: localize('interactiveSession.focusInput.label', "Focus Input"),
original: 'Focus Input'
},
f1: false,
keybinding: {
primary: KeyMod.CtrlCmd | KeyCode.DownArrow,
weight: KeybindingWeight.WorkbenchContrib,
when: ContextKeyExpr.and(CONTEXT_IN_INTERACTIVE_SESSION, ContextKeyExpr.not(EditorContextKeys.focus.key))
}
});
}
run(accessor: ServicesAccessor, ...args: any[]) {
const viewsService = accessor.get(IViewsService);
const interactiveSessionView = viewsService.getActiveViewWithId(InteractiveSessionViewPane.ID) as InteractiveSessionViewPane;
if (interactiveSessionView) {
interactiveSessionView.focus();
}
}
});
registerAction2(class ClearAction extends Action2 {
constructor() {
super(ClearInteractiveSessionActionDescriptor);
}
run(accessor: ServicesAccessor, ...args: any[]) {
// TODO hacks
InteractiveSessionViewPane.instances.forEach(instance => instance.clear());
}
});
}
export function getOpenInteractiveSessionEditorAction(id: string, label: string) {
return class OpenInteractiveSessionEditor extends Action2 {
constructor() {
super({
id: `workbench.action.openInteractiveSession.${id}`,
title: { value: localize('interactiveSession.open', "Open Interactive Session Editor ({0})", label), original: `Open Interactive Session Editor (${label})` },
f1: true,
category
});
}
async run(accessor: ServicesAccessor) {
const editorService = accessor.get(IEditorService);
await editorService.openEditor({ resource: InteractiveSessionEditor.getNewEditorUri(), options: <IInteractiveSessionEditorOptions>{ providerId: id } });
}
};
}

View file

@ -0,0 +1,129 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Codicon } from 'vs/base/common/codicons';
import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
import * as resources from 'vs/base/common/resources';
import { localize } from 'vs/nls';
import { MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { Registry } from 'vs/platform/registry/common/platform';
import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer';
import { IViewContainersRegistry, IViewDescriptor, IViewsRegistry, ViewContainer, ViewContainerLocation, Extensions as ViewExtensions } from 'vs/workbench/common/views';
import { ClearInteractiveSessionActionDescriptor, getOpenInteractiveSessionEditorAction } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionActions';
import { INTERACTIVE_SIDEBAR_PANEL_ID, InteractiveSessionViewPane, IInteractiveSessionViewOptions } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionSidebar';
import { IInteractiveSessionContributionService, IInteractiveSessionProviderContribution } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionContributionService';
import * as extensionsRegistry from 'vs/workbench/services/extensions/common/extensionsRegistry';
const interactiveSessionExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint<IInteractiveSessionProviderContribution[]>({
extensionPoint: 'interactiveSession',
jsonSchema: {
description: localize('vscode.extension.contributes.interactiveSession', 'Contributes an Interactive Session provider'),
type: 'array',
items: {
additionalProperties: false,
type: 'object',
defaultSnippets: [{ body: { id: '', program: '', runtime: '' } }],
properties: {
id: {
description: localize('vscode.extension.contributes.interactiveSession.id', "Unique identifier for this Interactive Session provider."),
type: 'string'
},
label: {
description: localize('vscode.extension.contributes.interactiveSession.label', "Display name for this Interactive Session provider."),
type: 'string'
},
icon: {
description: localize('vscode.extension.contributes.interactiveSession.icon', "An icon for this Interactive Session provider."),
type: 'string'
},
}
}
}
});
export class InteractiveSessionContributionService implements IInteractiveSessionContributionService {
declare _serviceBrand: undefined;
private _registrationDisposables = new Map<string, IDisposable>();
private _registeredProviders = new Map<string, IInteractiveSessionProviderContribution>();
constructor() {
interactiveSessionExtensionPoint.setHandler((extensions, delta) => {
for (const extension of delta.added) {
const extensionDisposable = new DisposableStore();
for (const providerDescriptor of extension.value) {
this.registerInteractiveSessionProvider(extension.description, providerDescriptor);
this._registeredProviders.set(providerDescriptor.id, providerDescriptor);
}
this._registrationDisposables.set(extension.description.identifier.value, extensionDisposable);
}
for (const extension of delta.removed) {
const registration = this._registrationDisposables.get(extension.description.identifier.value);
if (registration) {
registration.dispose();
this._registrationDisposables.delete(extension.description.identifier.value);
}
for (const providerDescriptor of extension.value) {
this._registeredProviders.delete(providerDescriptor.id);
}
}
});
}
public get registeredProviders(): IInteractiveSessionProviderContribution[] {
return Array.from(this._registeredProviders.values());
}
private registerInteractiveSessionProvider(extension: Readonly<IRelaxedExtensionDescription>, providerDescriptor: IInteractiveSessionProviderContribution): IDisposable {
// Register View Container
const viewContainerId = INTERACTIVE_SIDEBAR_PANEL_ID + '.' + providerDescriptor.id;
const viewContainer: ViewContainer = Registry.as<IViewContainersRegistry>(ViewExtensions.ViewContainersRegistry).registerViewContainer({
id: viewContainerId,
title: providerDescriptor.label,
icon: providerDescriptor.icon !== '' ? resources.joinPath(extension.extensionLocation, providerDescriptor.icon) : Codicon.commentDiscussion,
ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [viewContainerId, { mergeViewWithContainerWhenSingleView: true }]),
storageId: viewContainerId,
hideIfEmpty: true,
order: 100,
}, ViewContainerLocation.Sidebar);
// Register View
const viewId = InteractiveSessionViewPane.ID + '.' + providerDescriptor.id;
const viewDescriptor: IViewDescriptor[] = [{
id: viewId,
name: providerDescriptor.label,
canToggleVisibility: false,
canMoveView: true,
ctorDescriptor: new SyncDescriptor(InteractiveSessionViewPane, [<IInteractiveSessionViewOptions>{ providerId: providerDescriptor.id }]),
when: ContextKeyExpr.deserialize(providerDescriptor.when),
}];
Registry.as<IViewsRegistry>(ViewExtensions.ViewsRegistry).registerViews(viewDescriptor, viewContainer);
// Clear action in view title
const menuItem = MenuRegistry.appendMenuItem(MenuId.ViewTitle, {
command: ClearInteractiveSessionActionDescriptor,
when: ContextKeyExpr.equals('view', viewId),
group: 'navigation',
order: 0
});
// "Open Interactive Session Editor" Action
const openEditor = registerAction2(getOpenInteractiveSessionEditorAction(providerDescriptor.id, providerDescriptor.label));
return {
dispose: () => {
Registry.as<IViewsRegistry>(ViewExtensions.ViewsRegistry).deregisterViews(viewDescriptor, viewContainer);
Registry.as<IViewContainersRegistry>(ViewExtensions.ViewContainersRegistry).deregisterViewContainer(viewContainer);
menuItem.dispose();
openEditor.dispose();
}
};
}
}

View file

@ -0,0 +1,90 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Dimension, IDomPosition } from 'vs/base/browser/dom';
import { CancellationToken } from 'vs/base/common/cancellation';
import { URI } from 'vs/base/common/uri';
import { IEditorOptions } from 'vs/platform/editor/common/editor';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { editorBackground } from 'vs/platform/theme/common/colorRegistry';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane';
import { IEditorOpenContext } from 'vs/workbench/common/editor';
import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme';
import { InteractiveSessionEditorInput } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionEditorInput';
import { InteractiveSessionWidget } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionWidget';
export interface IInteractiveSessionEditorOptions extends IEditorOptions {
providerId: string;
}
export class InteractiveSessionEditor extends EditorPane {
static readonly ID: string = 'workbench.editor.interactiveSession';
static readonly SCHEME: string = 'interactiveSession';
private static _counter = 0;
static getNewEditorUri(): URI {
return URI.from({ scheme: InteractiveSessionEditor.SCHEME, path: `interactiveSession-${InteractiveSessionEditor._counter++}` });
}
private widget: InteractiveSessionWidget | undefined;
private parentElement: HTMLElement | undefined;
private dimension: Dimension | undefined;
constructor(
@ITelemetryService telemetryService: ITelemetryService,
@IThemeService themeService: IThemeService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IStorageService storageService: IStorageService,
) {
super(InteractiveSessionEditor.ID, telemetryService, themeService, storageService);
}
public clear() {
if (this.widget) {
this.widget.clear();
}
}
protected override createEditor(parent: HTMLElement): void {
this.parentElement = parent;
}
public override focus(): void {
if (this.widget) {
this.widget.focusInput();
}
}
override async setInput(input: InteractiveSessionEditorInput, options: IInteractiveSessionEditorOptions, context: IEditorOpenContext, token: CancellationToken): Promise<void> {
super.setInput(input, options, context, token);
// TODO would be much cleaner if I can create the widget first and set its provider id later
if (!this.widget) {
this.widget = this.instantiationService.createInstance(InteractiveSessionWidget, options.providerId, undefined, () => editorBackground, () => SIDE_BAR_BACKGROUND, () => SIDE_BAR_BACKGROUND);
if (!this.parentElement) {
throw new Error('InteractiveSessionEditor lifecycle issue: Parent element not set');
}
this.widget.render(this.parentElement);
this.widget.setVisible(true);
if (this.dimension) {
this.layout(this.dimension, undefined);
}
}
}
override layout(dimension: Dimension, position?: IDomPosition | undefined): void {
if (this.widget) {
this.widget.layout(dimension.height, dimension.width);
}
this.dimension = dimension;
}
}

View file

@ -0,0 +1,38 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI } from 'vs/base/common/uri';
import * as nls from 'vs/nls';
import { IUntypedEditorInput } from 'vs/workbench/common/editor';
import { EditorInput } from 'vs/workbench/common/editor/editorInput';
import { InteractiveSessionEditor } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionEditor';
export class InteractiveSessionEditorInput extends EditorInput {
static readonly ID: string = 'workbench.input.interactiveSession';
constructor(readonly resource: URI) {
super();
}
override get editorId(): string | undefined {
return InteractiveSessionEditor.ID;
}
override matches(otherInput: EditorInput | IUntypedEditorInput): boolean {
return otherInput instanceof InteractiveSessionEditorInput && otherInput.resource.toString() === this.resource.toString();
}
override get typeId(): string {
return InteractiveSessionEditorInput.ID;
}
override getName(): string {
return nls.localize('interactiveSessionEditorName', "Interactive Session");
}
override async resolve(): Promise<null> {
return null;
}
}

View file

@ -0,0 +1,478 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as dom from 'vs/base/browser/dom';
import { Button } from 'vs/base/browser/ui/button/button';
import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
import { ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree';
import { IntervalTimer } from 'vs/base/common/async';
import { Codicon } from 'vs/base/common/codicons';
import { Emitter, Event } from 'vs/base/common/event';
import { FuzzyScore } from 'vs/base/common/filters';
import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent';
import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
import { ThemeIcon } from 'vs/base/common/themables';
import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions';
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget';
import { EDITOR_FONT_DEFAULTS } from 'vs/editor/common/config/editorOptions';
import { Range } from 'vs/editor/common/core/range';
import { ILanguageService } from 'vs/editor/common/languages/language';
import { ITextModel } from 'vs/editor/common/model';
import { IModelService } from 'vs/editor/common/services/model';
import { BracketMatchingController } from 'vs/editor/contrib/bracketMatching/browser/bracketMatching';
import { ContextMenuController } from 'vs/editor/contrib/contextmenu/browser/contextmenu';
import { IMarkdownRenderResult, MarkdownRenderer } from 'vs/editor/contrib/markdownRenderer/browser/markdownRenderer';
import { ViewportSemanticTokensContribution } from 'vs/editor/contrib/semanticTokens/browser/viewportSemanticTokens';
import { SmartSelectController } from 'vs/editor/contrib/smartSelect/browser/smartSelect';
import { localize } from 'vs/nls';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ILogService } from 'vs/platform/log/common/log';
import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles';
import { FloatingClickMenu } from 'vs/workbench/browser/codeeditor';
import { InteractiveSessionInputOptions } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionOptions';
import { IInteractiveRequestViewModel, IInteractiveResponseViewModel, isRequestVM, isResponseVM } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionViewModel';
import { MenuPreventer } from 'vs/workbench/contrib/codeEditor/browser/menuPreventer';
import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEditor/browser/selectionClipboard';
import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions';
const $ = dom.$;
export type InteractiveTreeItem = IInteractiveRequestViewModel | IInteractiveResponseViewModel;
interface IInteractiveListItemTemplate {
rowContainer: HTMLElement;
header: HTMLElement;
avatar: HTMLElement;
username: HTMLElement;
value: HTMLElement;
elementDisposables: DisposableStore;
}
interface IItemHeightChangeParams {
element: InteractiveTreeItem;
height: number;
}
const wordRenderRate = 8; // words/sec
const enableVerboseLayoutTracing = false;
export class InteractiveListItemRenderer extends Disposable implements ITreeRenderer<InteractiveTreeItem, FuzzyScore, IInteractiveListItemTemplate> {
static readonly cursorCharacter = '\u258c';
static readonly ID = 'item';
private readonly renderer: MarkdownRenderer;
protected readonly _onDidChangeItemHeight = this._register(new Emitter<IItemHeightChangeParams>());
readonly onDidChangeItemHeight: Event<IItemHeightChangeParams> = this._onDidChangeItemHeight.event;
protected readonly _onDidSelectFollowup = this._register(new Emitter<string>());
readonly onDidSelectFollowup: Event<string> = this._onDidSelectFollowup.event;
private readonly _editorPool: EditorPool;
private _currentLayoutWidth: number = 0;
constructor(
private readonly options: InteractiveSessionInputOptions,
private readonly delegate: { getListLength(): number },
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IConfigurationService private readonly configService: IConfigurationService,
@ILogService private readonly logService: ILogService,
) {
super();
this.renderer = this.instantiationService.createInstance(MarkdownRenderer, {});
this._editorPool = this._register(this.instantiationService.createInstance(EditorPool, this.options));
}
get templateId(): string {
return InteractiveListItemRenderer.ID;
}
private traceLayout(method: string, message: string) {
if (enableVerboseLayoutTracing) {
this.logService.info(`${method}: ${message}`);
}
}
private shouldRenderProgressively(): boolean {
return this.configService.getValue('interactive.experimental.progressiveRendering');
}
layout(width: number): void {
this._currentLayoutWidth = width;
this._editorPool.inUse.forEach(editor => {
editor.layout(width);
});
}
renderTemplate(container: HTMLElement): IInteractiveListItemTemplate {
const rowContainer = dom.append(container, $('.interactive-item-container'));
const header = dom.append(rowContainer, $('.header'));
const avatar = dom.append(header, $('.avatar'));
const username = document.createElement('h3');
header.appendChild(username);
const value = dom.append(rowContainer, $('.value'));
const elementDisposables = new DisposableStore();
const template: IInteractiveListItemTemplate = { header, avatar, username, value, rowContainer, elementDisposables };
return template;
}
renderElement(node: ITreeNode<InteractiveTreeItem, FuzzyScore>, index: number, templateData: IInteractiveListItemTemplate): void {
const { element } = node;
const kind = isRequestVM(element) ? 'request' : 'response';
this.traceLayout('renderElement', `${kind}, index=${index}`);
templateData.rowContainer.classList.toggle('interactive-request', isRequestVM(element));
templateData.rowContainer.classList.toggle('interactive-response', isResponseVM(element));
templateData.username.textContent = isRequestVM(element) ? localize('username', "Username") : localize('response', "Response");
const avatarIcon = dom.$(ThemeIcon.asCSSSelector(isRequestVM(element) ? Codicon.account : Codicon.hubot));
templateData.avatar.replaceChildren(avatarIcon);
if (isResponseVM(element) && index === this.delegate.getListLength() - 1 && (!element.isComplete || element.renderData) && this.shouldRenderProgressively()) {
this.traceLayout('renderElement', `start progressive render ${kind}, index=${index}`);
const progressiveRenderingDisposables = templateData.elementDisposables.add(new DisposableStore());
const timer = templateData.elementDisposables.add(new IntervalTimer());
const runProgressiveRender = () => {
progressiveRenderingDisposables.clear();
const toRender = this.getProgressiveMarkdownToRender(element);
if (toRender) {
if (element.renderData?.isFullyRendered) {
this.traceLayout('runProgressiveRender', `end progressive render ${kind}, index=${index}`);
progressiveRenderingDisposables.clear();
this.basicRenderElement(element.response.value, element, index, templateData);
timer.cancel();
} else {
const plusCursor = toRender.match(/```.*$/) ? toRender + `\n${InteractiveListItemRenderer.cursorCharacter}` : toRender + ` ${InteractiveListItemRenderer.cursorCharacter}`;
const result = this.renderMarkdown(element, index, new MarkdownString(plusCursor), progressiveRenderingDisposables, templateData, true);
dom.clearNode(templateData.value);
templateData.value.appendChild(result.element);
progressiveRenderingDisposables.add(result);
}
}
};
runProgressiveRender();
timer.cancelAndSet(runProgressiveRender, 1000 / wordRenderRate);
} else if (isResponseVM(element)) {
this.basicRenderElement(element.response.value, element, index, templateData);
} else {
this.basicRenderElement(element.model.message, element, index, templateData);
}
}
private basicRenderElement(markdownValue: string, element: InteractiveTreeItem, index: number, templateData: IInteractiveListItemTemplate) {
const result = this.renderMarkdown(element, index, new MarkdownString(markdownValue), templateData.elementDisposables, templateData);
dom.clearNode(templateData.value);
templateData.value.appendChild(result.element);
templateData.elementDisposables.add(result);
if (isResponseVM(element) && element.followups?.length && index === this.delegate.getListLength() - 1) {
const followupsContainer = dom.append(templateData.value, $('.interactive-response-followups'));
element.followups.forEach(q => {
const button = templateData.elementDisposables.add(new Button(followupsContainer, defaultButtonStyles));
button.label = `"${q}"`;
templateData.elementDisposables.add(button.onDidClick(() => this._onDidSelectFollowup.fire(q)));
return button;
});
}
}
private renderMarkdown(element: InteractiveTreeItem, index: number, markdown: IMarkdownString, disposables: DisposableStore, templateData: IInteractiveListItemTemplate, fillInIncompleteTokens = false): IMarkdownRenderResult {
const notifyHeightChange = () => {
const height = templateData.rowContainer.clientHeight;
if (height && (typeof element.currentRenderedHeight === 'undefined' || element.currentRenderedHeight !== height)) {
element.currentRenderedHeight = height;
this.traceLayout('notifyHeightChange', `index=${index}, height=${height}`);
this._onDidChangeItemHeight.fire({ element, height });
}
};
let didRenderEditor = false;
const disposablesList: IDisposable[] = [];
const result = this.renderer.render(markdown, {
fillInIncompleteTokens,
codeBlockRenderer: async (languageId, value) => {
didRenderEditor = true;
const editorInfo = this._editorPool.get();
disposablesList.push(editorInfo);
editorInfo.setText(value);
editorInfo.setLanguage(languageId);
const layoutEditor = (context: string) => {
editorInfo.layout(this._currentLayoutWidth);
};
layoutEditor('init');
disposables.add(editorInfo.textModel.onDidChangeContent(() => {
layoutEditor('textmodel');
}));
return editorInfo.element;
},
asyncRenderCallback: () => {
// do updateElementHeight when all editors have been rendered and layed out
notifyHeightChange();
}
});
if (!didRenderEditor) {
disposables.add(dom.scheduleAtNextAnimationFrame(() => {
notifyHeightChange();
}));
}
disposablesList.reverse().forEach(d => disposables.add(d));
return result;
}
private getProgressiveMarkdownToRender(element: IInteractiveResponseViewModel): string | undefined {
const renderData = element.renderData ?? { renderPosition: 0, renderTime: 0 };
const numWordsToRender = renderData.renderTime === 0 ?
1 :
renderData.renderPosition + Math.floor((Date.now() - renderData.renderTime) / 1000 * wordRenderRate);
if (numWordsToRender === renderData.renderPosition) {
return undefined;
}
let wordCount = numWordsToRender;
let i = 0;
const wordSeparatorCharPattern = /[\s\|\-]/;
while (i < element.response.value.length && wordCount > 0) {
// Consume word separator chars
while (i < element.response.value.length && element.response.value[i].match(wordSeparatorCharPattern)) {
i++;
}
// Consume word chars
while (i < element.response.value.length && !element.response.value[i].match(wordSeparatorCharPattern)) {
i++;
}
wordCount--;
}
const value = element.response.value.substring(0, i);
element.renderData = {
renderPosition: numWordsToRender - wordCount,
renderTime: Date.now(),
isFullyRendered: i >= element.response.value.length
};
return value;
}
disposeElement(node: ITreeNode<InteractiveTreeItem, FuzzyScore>, index: number, templateData: IInteractiveListItemTemplate): void {
templateData.elementDisposables.clear();
}
disposeTemplate(templateData: IInteractiveListItemTemplate): void {
}
}
export class InteractiveSessionListDelegate implements IListVirtualDelegate<InteractiveTreeItem> {
constructor(
@ILogService private readonly logService: ILogService
) { }
private _traceLayout(method: string, message: string) {
if (enableVerboseLayoutTracing) {
this.logService.info(`InteractiveSessionListDelegate#${method}: ${message}`);
}
}
getHeight(element: InteractiveTreeItem): number {
const kind = isRequestVM(element) ? 'request' : 'response';
const height = element.currentRenderedHeight ?? 40;
this._traceLayout('getHeight', `${kind}, height=${height}`);
return height;
}
getTemplateId(element: InteractiveTreeItem): string {
return InteractiveListItemRenderer.ID;
}
}
export class InteractiveSessionAccessibilityProvider implements IListAccessibilityProvider<InteractiveTreeItem> {
getWidgetAriaLabel(): string {
return localize('interactiveSession', "Interactive Session");
}
getAriaLabel(element: InteractiveTreeItem): string {
if (isRequestVM(element)) {
return localize('interactiveRequest', "Request: {0}", element.model.message);
}
if (isResponseVM(element)) {
return localize('interactiveResponse', "Response: {0}", element.response.value);
}
return '';
}
}
interface IInteractiveResultEditorInfo {
readonly element: HTMLElement;
readonly textModel: ITextModel;
layout(width: number): void;
setLanguage(langugeId: string): void;
setText(text: string): void;
dispose(): void;
}
class EditorPool extends Disposable {
private _pool: ResourcePool<IInteractiveResultEditorInfo>;
public get inUse(): ReadonlySet<IInteractiveResultEditorInfo> {
return this._pool.inUse;
}
constructor(
private readonly options: InteractiveSessionInputOptions,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@ILanguageService private readonly languageService: ILanguageService,
@IModelService private readonly modelService: IModelService,
) {
super();
this._pool = this._register(new ResourcePool(() => this.editorFactory()));
// TODO listen to changes on options
}
private editorFactory(): IInteractiveResultEditorInfo {
const disposables = new DisposableStore();
const wrapper = $('.interactive-result-editor-wrapper');
const editor = disposables.add(this.instantiationService.createInstance(CodeEditorWidget, wrapper, {
...getSimpleEditorOptions(),
readOnly: true,
wordWrap: 'off',
lineNumbers: 'off',
selectOnLineNumbers: true,
scrollBeyondLastLine: false,
lineDecorationsWidth: 8,
dragAndDrop: false,
bracketPairColorization: this.options.configuration.resultEditor.bracketPairColorization,
padding: { top: 2, bottom: 2 },
fontFamily: this.options.configuration.resultEditor.fontFamily === 'default' ? EDITOR_FONT_DEFAULTS.fontFamily : this.options.configuration.resultEditor.fontFamily,
fontSize: this.options.configuration.resultEditor.fontSize,
fontWeight: this.options.configuration.resultEditor.fontWeight,
lineHeight: this.options.configuration.resultEditor.lineHeight,
mouseWheelZoom: false,
scrollbar: {
alwaysConsumeMouseWheel: false
}
}, {
isSimpleWidget: false,
contributions: EditorExtensionsRegistry.getSomeEditorContributions([
MenuPreventer.ID,
SelectionClipboardContributionID,
ContextMenuController.ID,
ViewportSemanticTokensContribution.ID,
BracketMatchingController.ID,
FloatingClickMenu.ID,
SmartSelectController.ID,
])
}));
const vscodeLanguageId = this.languageService.getLanguageIdByLanguageName('javascript');
const textModel = disposables.add(this.modelService.createModel('', this.languageService.createById(vscodeLanguageId), undefined));
editor.setModel(textModel);
return {
element: wrapper,
textModel,
layout: (width: number) => {
const realContentHeight = editor.getContentHeight();
editor.layout({ width, height: realContentHeight });
},
setText: (newText: string) => {
let currentText = textModel.getLinesContent().join('\n');
if (newText === currentText) {
return;
}
let removedChars = 0;
if (currentText.endsWith(` ${InteractiveListItemRenderer.cursorCharacter}`)) {
removedChars = 2;
} else if (currentText.endsWith(InteractiveListItemRenderer.cursorCharacter)) {
removedChars = 1;
}
if (removedChars > 0) {
currentText = currentText.slice(0, currentText.length - removedChars);
}
if (newText.startsWith(currentText)) {
const text = newText.slice(currentText.length);
const lastLine = textModel.getLineCount();
const lastCol = textModel.getLineMaxColumn(lastLine);
const insertAtCol = lastCol - removedChars;
textModel.applyEdits([{ range: new Range(lastLine, insertAtCol, lastLine, lastCol), text }]);
} else {
// console.log(`Failed to optimize setText`);
textModel.setValue(newText);
}
},
setLanguage: (languageId: string) => {
const vscodeLanguageId = this.languageService.getLanguageIdByLanguageName(languageId);
if (vscodeLanguageId) {
textModel.setLanguage(vscodeLanguageId);
}
},
dispose: () => {
disposables.dispose();
}
};
}
get(): IInteractiveResultEditorInfo {
const object = this._pool.get();
return {
...object,
dispose: () => this._pool.release(object)
};
}
}
class ResourcePool<T extends IDisposable> extends Disposable {
private readonly pool: T[] = [];
private _inUse = new Set<T>;
public get inUse(): ReadonlySet<T> {
return this._inUse;
}
constructor(
private readonly _itemFactory: () => T,
) {
super();
}
get(): T {
if (this.pool.length > 0) {
const item = this.pool.pop()!;
this._inUse.add(item);
return item;
}
const item = this._register(this._itemFactory());
this._inUse.add(item);
return item;
}
release(item: T): void {
this._inUse.delete(item);
this.pool.push(item);
}
}

View file

@ -0,0 +1,117 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Color } from 'vs/base/common/color';
import { Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { IBracketPairColorizationOptions } from 'vs/editor/common/config/editorOptions';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IViewDescriptorService } from 'vs/workbench/common/views';
export interface IInteractiveSessionConfiguration {
editor: {
readonly fontSize: number;
readonly fontFamily: string;
readonly lineHeight: number;
readonly fontWeight: string;
};
}
export interface IInteractiveSessionEditorOptions {
readonly inputEditor: IInteractiveSessionInputEditorOptions;
readonly resultEditor: IInteractiveSessionResultEditorOptions;
}
export interface IInteractiveSessionInputEditorOptions {
readonly backgroundColor: Color | undefined;
readonly accessibilitySupport: string;
}
export interface IInteractiveSessionResultEditorOptions {
readonly fontSize: number;
readonly fontFamily: string;
readonly lineHeight: number;
readonly fontWeight: string;
readonly backgroundColor: Color | undefined;
readonly bracketPairColorization: IBracketPairColorizationOptions;
// Bring these back if we make the editors editable
// readonly cursorBlinking: string;
// readonly accessibilitySupport: string;
}
export class InteractiveSessionInputOptions extends Disposable {
private static readonly lineHeightEm = 1.4;
private readonly _onDidChange = this._register(new Emitter<void>());
readonly onDidChange = this._onDidChange.event;
private _config!: IInteractiveSessionEditorOptions;
public get configuration(): IInteractiveSessionEditorOptions {
return this._config;
}
private static readonly relevantSettingIds = [
'interactiveSession.editor.lineHeight',
'interactiveSession.editor.fontSize',
'interactiveSession.editor.fontFamily',
'interactiveSession.editor.fontWeight',
'editor.cursorBlinking',
'editor.accessibilitySupport',
'editor.bracketPairColorization.enabled',
'editor.bracketPairColorization.independentColorPoolPerBracketType',
];
constructor(
viewId: string | undefined,
private readonly inputEditorBackgroundColorDelegate: () => string,
private readonly resultEditorBackgroundColorDelegate: () => string,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IThemeService private readonly themeService: IThemeService,
@IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService
) {
super();
this._register(this.themeService.onDidColorThemeChange(e => this.update()));
this._register(this.viewDescriptorService.onDidChangeLocation(e => {
if (e.views.some(v => v.id === viewId)) {
this.update();
}
}));
this._register(this.configurationService.onDidChangeConfiguration(e => {
if (InteractiveSessionInputOptions.relevantSettingIds.some(id => e.affectsConfiguration(id))) {
this.update();
}
}));
this.update();
}
private update() {
const editorConfig = this.configurationService.getValue<any>('editor');
const interactiveSessionSidebarEditor = this.configurationService.getValue<IInteractiveSessionConfiguration>('interactiveSession').editor;
const accessibilitySupport = this.configurationService.getValue<'auto' | 'off' | 'on'>('editor.accessibilitySupport');
this._config = {
inputEditor: {
backgroundColor: this.themeService.getColorTheme().getColor(this.inputEditorBackgroundColorDelegate()),
accessibilitySupport,
},
resultEditor: {
backgroundColor: this.themeService.getColorTheme().getColor(this.resultEditorBackgroundColorDelegate()),
fontSize: interactiveSessionSidebarEditor.fontSize,
fontFamily: interactiveSessionSidebarEditor.fontFamily === 'default' ? editorConfig.fontFamily : interactiveSessionSidebarEditor.fontFamily,
fontWeight: interactiveSessionSidebarEditor.fontWeight,
lineHeight: interactiveSessionSidebarEditor.lineHeight ? interactiveSessionSidebarEditor.lineHeight : InteractiveSessionInputOptions.lineHeightEm * interactiveSessionSidebarEditor.fontSize,
bracketPairColorization: {
enabled: this.configurationService.getValue<boolean>('editor.bracketPairColorization.enabled'),
independentColorPoolPerBracketType: this.configurationService.getValue<boolean>('editor.bracketPairColorization.independentColorPoolPerBracketType'),
},
}
};
this._onDidChange.fire();
}
}

View file

@ -0,0 +1,81 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { editorBackground } from 'vs/platform/theme/common/colorRegistry';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPane';
import { IViewDescriptorService } from 'vs/workbench/common/views';
import { InteractiveSessionWidget } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionWidget';
export interface IInteractiveSessionViewOptions {
readonly providerId: string;
}
export const INTERACTIVE_SIDEBAR_PANEL_ID = 'workbench.panel.interactiveSessionSidebar';
export class InteractiveSessionViewPane extends ViewPane {
static instances: InteractiveSessionViewPane[] = [];
static ID = 'workbench.panel.interactiveSession.view';
private view: InteractiveSessionWidget;
constructor(
interactivSessionViewOptions: IInteractiveSessionViewOptions,
options: IViewPaneOptions,
@IKeybindingService keybindingService: IKeybindingService,
@IContextMenuService contextMenuService: IContextMenuService,
@IConfigurationService configurationService: IConfigurationService,
@IContextKeyService contextKeyService: IContextKeyService,
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
@IInstantiationService instantiationService: IInstantiationService,
@IOpenerService openerService: IOpenerService,
@IThemeService themeService: IThemeService,
@ITelemetryService telemetryService: ITelemetryService,
) {
super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService);
// TODO hacks
InteractiveSessionViewPane.instances.push(this);
this.view = this._register(this.instantiationService.createInstance(InteractiveSessionWidget, interactivSessionViewOptions.providerId, this.id, () => this.getBackgroundColor(), () => this.getBackgroundColor(), () => editorBackground));
this._register(this.onDidChangeBodyVisibility(visible => {
this.view.setVisible(visible);
}));
}
protected override renderBody(parent: HTMLElement): void {
super.renderBody(parent);
this.view.render(parent);
}
acceptInput(): void {
this.view.acceptInput();
}
clear(): void {
this.view.clear();
}
focusInput(): void {
this.view.focusInput();
}
override focus(): void {
super.focus();
this.view.focusInput();
}
protected override layoutBody(height: number, width: number): void {
super.layoutBody(height, width);
this.view.layout(height, width);
}
}

View file

@ -0,0 +1,356 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as dom from 'vs/base/browser/dom';
import { Button } from 'vs/base/browser/ui/button/button';
import { ITreeElement } from 'vs/base/browser/ui/tree/tree';
import { CancellationToken } from 'vs/base/common/cancellation';
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import 'vs/css!./media/interactiveSession';
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget';
import { ITextModel } from 'vs/editor/common/model';
import { IModelService } from 'vs/editor/common/services/model';
import { localize } from 'vs/nls';
import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { WorkbenchObjectTree } from 'vs/platform/list/browser/listService';
import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles';
import { foreground } from 'vs/platform/theme/common/colorRegistry';
import { DEFAULT_FONT_FAMILY } from 'vs/workbench/browser/style';
import { InteractiveListItemRenderer, InteractiveSessionAccessibilityProvider, InteractiveSessionListDelegate, InteractiveTreeItem } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionListRenderer';
import { InteractiveSessionInputOptions } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionOptions';
import { IInteractiveSessionService } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionService';
import { InteractiveSessionViewModel, IInteractiveSessionViewModel, isRequestVM, isResponseVM } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionViewModel';
import { getSimpleCodeEditorWidgetOptions, getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
const $ = dom.$;
export const CONTEXT_IN_INTERACTIVE_INPUT = new RawContextKey<boolean>('inInteractiveInput', false, { type: 'boolean', description: localize('inInteractiveInput', "True when focus is in the interactive input, false otherwise.") });
export const CONTEXT_IN_INTERACTIVE_SESSION = new RawContextKey<boolean>('inInteractiveSession', false, { type: 'boolean', description: localize('inInteractiveSession', "True when focus is in the interactive session widget, false otherwise.") });
function revealLastElement(list: WorkbenchObjectTree<any>) {
list.scrollTop = list.scrollHeight - list.renderHeight;
}
export class InteractiveSessionWidget extends Disposable {
private static readonly widgetsByInputUri = new Map<string, InteractiveSessionWidget>();
static getViewByInputUri(inputUri: URI): InteractiveSessionWidget | undefined {
return InteractiveSessionWidget.widgetsByInputUri.get(inputUri.toString());
}
private static _counter = 0;
private readonly inputUri = URI.parse(`interactiveSessionInput:input-${InteractiveSessionWidget._counter++}`);
private tree!: WorkbenchObjectTree<InteractiveTreeItem>;
private renderer!: InteractiveListItemRenderer;
private inputEditor!: CodeEditorWidget;
private inputOptions!: InteractiveSessionInputOptions;
private inputModel: ITextModel | undefined;
private listContainer!: HTMLElement;
private container!: HTMLElement;
private welcomeViewContainer!: HTMLElement;
private welcomeViewDisposables = this._register(new DisposableStore());
private bodyDimension: dom.Dimension | undefined;
private visible = false;
private previousTreeScrollHeight: number = 0;
private viewModel: IInteractiveSessionViewModel | undefined;
private viewModelDisposables = new DisposableStore();
constructor(
private readonly providerId: string,
private readonly viewId: string | undefined,
private readonly listBackgroundColorDelegate: () => string,
private readonly inputEditorBackgroundColorDelegate: () => string,
private readonly resultEditorBackgroundColorDelegate: () => string,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IModelService private readonly modelService: IModelService,
@IExtensionService private readonly extensionService: IExtensionService,
@IInteractiveSessionService private readonly interactiveSessionService: IInteractiveSessionService
) {
super();
CONTEXT_IN_INTERACTIVE_SESSION.bindTo(contextKeyService).set(true);
InteractiveSessionWidget.widgetsByInputUri.set(this.inputUri.toString(), this);
this.initializeSessionModel(true);
}
render(parent: HTMLElement): void {
this.container = dom.append(parent, $('.interactive-session'));
this.listContainer = dom.append(this.container, $(`.interactive-list`));
this.inputOptions = this._register(this.instantiationService.createInstance(InteractiveSessionInputOptions, this.viewId, this.inputEditorBackgroundColorDelegate, this.resultEditorBackgroundColorDelegate));
this.renderWelcomeView(this.container);
this.createList(this.listContainer);
this.createInput(this.container);
this._register(this.inputOptions.onDidChange(() => this.onDidStyleChange()));
this.onDidStyleChange();
// Do initial render
if (this.viewModel) {
this.onDidChangeItems();
}
}
focusInput(): void {
this.inputEditor.focus();
}
private onDidChangeItems() {
if (this.tree && this.visible && this.viewModel) {
const items = this.viewModel.getItems();
const treeItems = items.map(item => {
return <ITreeElement<InteractiveTreeItem>>{
element: item,
collapsed: false,
collapsible: false
};
});
if (treeItems.length) {
this.setWelcomeViewVisible(false);
const lastItem = treeItems[treeItems.length - 1];
this.tree.setChildren(null, treeItems, {
diffIdentityProvider: {
getId(element) {
const isLastAndResponse = isResponseVM(element) && element === lastItem.element;
return element.id + (isLastAndResponse ? '_last' : '');
},
}
});
revealLastElement(this.tree);
}
}
}
setVisible(visible: boolean): void {
this.visible = visible;
if (visible) {
if (!this.inputModel) {
this.inputModel = this.modelService.getModel(this.inputUri) || this.modelService.createModel('', null, this.inputUri, true);
}
this.setModeAsync();
this.inputEditor.setModel(this.inputModel);
// Not sure why this is needed- the view is being rendered before it's visible, and then the list content doesn't show up
this.onDidChangeItems();
}
}
private onDidStyleChange(): void {
this.container.style.setProperty('--vscode-interactive-result-editor-background-color', this.inputOptions.configuration.resultEditor.backgroundColor?.toString() ?? '');
}
private setModeAsync(): void {
this.extensionService.whenInstalledExtensionsRegistered().then(() => {
this.inputModel!.setLanguage('markdown');
});
}
private async renderWelcomeView(container: HTMLElement): Promise<void> {
if (this.welcomeViewContainer) {
dom.clearNode(this.welcomeViewContainer);
} else {
this.welcomeViewContainer = dom.append(container, $('.interactive-session-welcome-view'));
}
this.welcomeViewDisposables.clear();
const suggestions = await this.interactiveSessionService.provideSuggestions(this.providerId, CancellationToken.None);
const suggElements = suggestions?.map(sugg => {
const button = this.welcomeViewDisposables.add(new Button(this.welcomeViewContainer, defaultButtonStyles));
button.label = `"${sugg}"`;
this.welcomeViewDisposables.add(button.onDidClick(() => this.acceptInput(sugg)));
return button;
});
if (suggElements && suggElements.length > 0) {
this.setWelcomeViewVisible(true);
} else {
this.setWelcomeViewVisible(false);
}
}
private setWelcomeViewVisible(visible: boolean): void {
if (visible) {
dom.show(this.welcomeViewContainer);
dom.hide(this.listContainer);
} else {
dom.hide(this.welcomeViewContainer);
dom.show(this.listContainer);
}
}
private createList(listContainer: HTMLElement): void {
const scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.contextKeyService]));
const delegate = scopedInstantiationService.createInstance(InteractiveSessionListDelegate);
this.renderer = scopedInstantiationService.createInstance(InteractiveListItemRenderer, this.inputOptions, { getListLength: () => this.tree.getNode(null).visibleChildrenCount });
this.tree = <WorkbenchObjectTree<InteractiveTreeItem>>scopedInstantiationService.createInstance(
WorkbenchObjectTree,
'InteractiveSession',
listContainer,
delegate,
[this.renderer],
{
identityProvider: { getId: (e: InteractiveTreeItem) => e.id },
supportDynamicHeights: false,
hideTwistiesOfChildlessElements: true,
accessibilityProvider: new InteractiveSessionAccessibilityProvider(),
keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: (e: InteractiveTreeItem) => isRequestVM(e) ? e.model.message : e.response.value },
setRowLineHeight: false,
overrideStyles: {
listFocusBackground: this.listBackgroundColorDelegate(),
listInactiveFocusBackground: this.listBackgroundColorDelegate(),
listActiveSelectionBackground: this.listBackgroundColorDelegate(),
listFocusAndSelectionBackground: this.listBackgroundColorDelegate(),
listInactiveSelectionBackground: this.listBackgroundColorDelegate(),
listHoverBackground: this.listBackgroundColorDelegate(),
listBackground: this.listBackgroundColorDelegate(),
listFocusForeground: foreground,
listHoverForeground: foreground,
listInactiveFocusForeground: foreground,
listInactiveSelectionForeground: foreground,
listActiveSelectionForeground: foreground,
listFocusAndSelectionForeground: foreground,
}
});
this._register(this.tree.onDidChangeContentHeight(() => {
this.onDidChangeTreeContentHeight();
}));
this._register(this.renderer.onDidChangeItemHeight(e => {
this.tree.updateElementHeight(e.element, e.height);
this.onDidChangeTreeContentHeight();
}));
this._register(this.renderer.onDidSelectFollowup(followup => {
this.acceptInput(followup);
}));
}
private onDidChangeTreeContentHeight(): void {
if (this.tree.scrollHeight !== this.previousTreeScrollHeight) {
// Due to rounding, the scrollTop + renderHeight will not exactly match the scrollHeight.
// Consider the tree to be scrolled all the way down if it is within 2px of the bottom.
// const lastElementWasVisible = this.list.scrollTop + this.list.renderHeight >= this.previousTreeScrollHeight - 2;
const lastElementWasVisible = this.tree.scrollTop + this.tree.renderHeight >= this.previousTreeScrollHeight;
if (lastElementWasVisible) {
setTimeout(() => {
// Can't set scrollTop during this event listener, the list might overwrite the change
revealLastElement(this.tree);
}, 0);
}
}
this.previousTreeScrollHeight = this.tree.scrollHeight;
}
private createInput(container: HTMLElement): void {
const inputContainer = dom.append(container, $('.interactive-input-wrapper'));
const inputScopedContextKeyService = this._register(this.contextKeyService.createScoped(inputContainer));
CONTEXT_IN_INTERACTIVE_INPUT.bindTo(inputScopedContextKeyService).set(true);
const scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, inputScopedContextKeyService]));
const options = getSimpleEditorOptions();
options.readOnly = false;
options.ariaLabel = localize('interactiveSessionInput', "Interactive Session Input");
options.fontFamily = DEFAULT_FONT_FAMILY;
options.fontSize = 13;
options.lineHeight = 20;
options.padding = { top: 8, bottom: 7 };
options.cursorWidth = 1;
this.inputEditor = this._register(scopedInstantiationService.createInstance(CodeEditorWidget, inputContainer, options, getSimpleCodeEditorWidgetOptions()));
this._register(this.inputEditor.onDidChangeModelContent(() => {
if (this.bodyDimension) {
this.layout(this.bodyDimension.height, this.bodyDimension.width);
}
}));
this._register(dom.addStandardDisposableListener(inputContainer, dom.EventType.FOCUS, () => inputContainer.classList.add('synthetic-focus')));
this._register(dom.addStandardDisposableListener(inputContainer, dom.EventType.BLUR, () => inputContainer.classList.remove('synthetic-focus')));
}
private async initializeSessionModel(initial = false) {
await this.extensionService.whenInstalledExtensionsRegistered();
const model = await this.interactiveSessionService.startSession(this.providerId, initial, CancellationToken.None);
if (!model) {
throw new Error('Failed to start session');
}
this.viewModel = new InteractiveSessionViewModel(model);
this.viewModelDisposables.add(this.viewModel.onDidChange(() => this.onDidChangeItems()));
this.viewModelDisposables.add(this.viewModel.onDidDispose(() => {
this.viewModel = undefined;
this.viewModelDisposables.clear();
this.onDidChangeItems();
}));
if (this.tree) {
this.onDidChangeItems();
}
}
async acceptInput(query?: string): Promise<void> {
if (!this.viewModel) {
await this.initializeSessionModel();
}
if (this.viewModel) {
const input = query ?? this.inputEditor.getValue();
if (this.interactiveSessionService.sendRequest(this.viewModel.sessionId, input, CancellationToken.None)) {
this.inputEditor.setValue('');
}
}
}
focusLastMessage(): void {
if (!this.viewModel) {
return;
}
const items = this.viewModel.getItems();
const lastItem = items[items.length - 1];
if (!lastItem) {
return;
}
this.tree.setFocus([lastItem]);
this.tree.domFocus();
}
clear(): void {
if (this.viewModel) {
this.interactiveSessionService.clearSession(this.viewModel.sessionId);
this.focusInput();
this.renderWelcomeView(this.container);
}
}
layout(height: number, width: number): void {
this.bodyDimension = new dom.Dimension(width, height);
const inputHeight = Math.min(this.inputEditor.getContentHeight(), height);
const inputWrapperPadding = 24;
const lastElementVisible = this.tree.scrollTop + this.tree.renderHeight >= this.tree.scrollHeight;
const listHeight = height - inputHeight - inputWrapperPadding;
this.tree.layout(listHeight, width);
this.tree.getHTMLElement().style.height = `${listHeight}px`;
this.renderer.layout(width);
if (lastElementVisible) {
revealLastElement(this.tree);
}
this.welcomeViewContainer.style.height = `${height - inputHeight - inputWrapperPadding}px`;
this.listContainer.style.height = `${height - inputHeight - inputWrapperPadding}px`;
this.inputEditor.layout({ width: width - inputWrapperPadding, height: inputHeight });
}
}

View file

@ -0,0 +1,180 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.interactive-list .monaco-list-row:not(:first-of-type) {
border-top: 1px solid var(--vscode-interactive-responseBorder);
}
.interactive-list .monaco-list-row:last-of-type {
border-bottom: 1px solid var(--vscode-interactive-responseBorder);
}
.interactive-list .monaco-list-row .monaco-tl-twistie {
/* Hide twisties */
display: none !important;
}
.interactive-list .interactive-item-container {
display: flex;
flex-direction: column;
gap: 12px;
}
.interactive-list .interactive-item-container .header {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
.interactive-list .interactive-item-container .header h3 {
margin: 0;
font-size: 12px;
font-weight: 600;
}
.interactive-list .interactive-item-container .header .avatar {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 50%;
background: var(--vscode-badge-background);
}
.interactive-list .interactive-item-container .header .avatar .codicon {
color: var(--vscode-badge-foreground);
}
.interactive-list .interactive-item-container .value {
width: 100%;
overflow: hidden;
}
.interactive-list .interactive-item-container .value table {
width: 100%;
text-align: left;
margin-bottom: 16px;
}
.interactive-list .interactive-item-container .value table,
.interactive-list .interactive-item-container .value table td,
.interactive-list .interactive-item-container .value table th {
border: 1px solid var(--vscode-interactive-responseBorder);
border-collapse: collapse;
padding: 4px 6px;
}
.interactive-list {
overflow: hidden;
}
.interactive-list .monaco-list-row .interactive-request,
.interactive-list .monaco-list-row .interactive-response {
user-select: text;
-webkit-user-select: text;
}
.interactive-list .monaco-list-row .interactive-response {
background-color: var(--vscode-interactive-responseBackground);
padding: 16px 20px 0 20px
}
.interactive-list .monaco-list-row .interactive-response .value {
padding-bottom: 12px;
}
.interactive-list .monaco-list-row .interactive-request {
padding: 16px 20px;
}
.interactive-list .monaco-list-row .value {
white-space: normal;
}
.interactive-list .monaco-list-row .value p {
margin: 0;
}
.interactive-list .monaco-list-row .monaco-tokenized-source,
.interactive-list .monaco-list-row code {
font-family: var(--monaco-monospace-font);
}
.interactive-session .interactive-input-wrapper {
display: flex;
border-radius: 2px;
box-sizing: border-box;
padding: 12px;
cursor: text;
border-top: 1px solid var(--vscode-interactive-responseBorder);
}
.interactive-session .interactive-input-wrapper .monaco-editor-background {
background-color: var(--vscode-input-background);
padding: 0 8px;
}
/* TODO @daviddossett only apply focus border on focus */
.interactive-session .interactive-input-wrapper .monaco-editor {
border: 1px solid var(--vscode-focusBorder);
}
.interactive-session .interactive-input-wrapper .monaco-editor,
.interactive-session .interactive-input-wrapper .monaco-editor .overflow-guard {
border-radius: 4px;
}
.interactive-session .interactive-input-wrapper .monaco-editor .cursors-layer {
padding-left: 4px;
}
.interactive-session .monaco-inputbox {
width: 100%;
}
.interactive-session .interactive-result-editor-wrapper .monaco-editor,
.interactive-session .interactive-result-editor-wrapper .monaco-editor .overflow-guard {
border-radius: 4px;
}
.interactive-session .interactive-response .monaco-editor .margin,
.interactive-session .interactive-response .monaco-editor .monaco-editor-background {
background-color: var(--vscode-interactive-result-editor-background-color);
}
.interactive-result-editor-wrapper {
margin: 16px 0;
}
.interactive-session .interactive-session-welcome-view {
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: start;
padding: 20px;
gap: 16px;
}
.interactive-session .interactive-session-welcome-view .monaco-button {
max-width: 400px;
margin: 0;
}
.interactive-session .interactive-response .interactive-session-response-followups {
display: flex;
flex-direction: column;
gap: 8px;
align-items: start;
margin-bottom: 1em; /* This is matching the margin on rendered markdown */
}
.interactive-session .interactive-response .interactive-session-response-followups .monaco-button {
width: 100%;
padding: 2px 8px;
font-size: 11px;
font-weight: 600;
}

View file

@ -0,0 +1,27 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Color, RGBA } from 'vs/base/common/color';
import { localize } from 'vs/nls';
import { registerColor } from 'vs/platform/theme/common/colorRegistry';
export const interactiveResponseBackground = registerColor(
'interactive.responseBackground',
{ dark: new Color(new RGBA(255, 255, 255, 0.03)), light: new Color(new RGBA(0, 0, 0, 0.03)), hcDark: null, hcLight: null, },
localize('interactive.responseBackground', 'The resting background color of an interactive response.')
);
export const interactiveResponseActiveBackground = registerColor(
'interactive.responseActiveBackground',
{ dark: new Color(new RGBA(255, 255, 255, 0.10)), light: new Color(new RGBA(0, 0, 0, 0.10)), hcDark: null, hcLight: null, },
localize('interactive.responseActiveBackground', 'The active background color of an interactive response. Used when the response shows a fade out animation on load.')
);
export const interactiveResponseBorder = registerColor(
'interactive.responseBorder',
{ dark: new Color(new RGBA(255, 255, 255, 0.10)), light: new Color(new RGBA(0, 0, 0, 0.10)), hcDark: null, hcLight: null, },
localize('interactive.responseBorder', 'The border color of an interactive response.')
);

View file

@ -0,0 +1,20 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
export const IInteractiveSessionContributionService = createDecorator<IInteractiveSessionContributionService>('IInteractiveSessionContributionService');
export interface IInteractiveSessionContributionService {
_serviceBrand: undefined;
registeredProviders: IInteractiveSessionProviderContribution[];
}
export interface IInteractiveSessionProviderContribution {
id: string;
label: string;
icon: string;
when?: string;
}

View file

@ -0,0 +1,214 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Emitter, Event } from 'vs/base/common/event';
import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent';
import { Disposable } from 'vs/base/common/lifecycle';
import { IInteractiveSession } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionService';
export interface IInteractiveRequestModel {
readonly id: string;
readonly message: string;
readonly response: IInteractiveResponseModel | undefined;
}
export interface IInteractiveResponseModel {
readonly onDidChange: Event<void>;
readonly id: string;
readonly response: IMarkdownString;
readonly isComplete: boolean;
readonly followups?: string[];
}
export function isRequest(item: unknown): item is IInteractiveRequestModel {
return !!item && typeof (item as IInteractiveRequestModel).message !== 'undefined';
}
export function isResponse(item: unknown): item is IInteractiveResponseModel {
return !isRequest(item);
}
export class InteractiveRequestModel implements IInteractiveRequestModel {
private static nextId = 0;
public response: InteractiveResponseModel | undefined;
private _id: string;
public get id(): string {
return this._id;
}
constructor(public readonly message: string) {
this._id = 'request_' + InteractiveRequestModel.nextId++;
}
}
export class InteractiveResponseModel extends Disposable implements IInteractiveResponseModel {
private readonly _onDidChange = this._register(new Emitter<void>());
readonly onDidChange = this._onDidChange.event;
private static nextId = 0;
private _id: string;
public get id(): string {
return this._id;
}
private _isComplete: boolean;
public get isComplete(): boolean {
return this._isComplete;
}
private _followups: string[] | undefined;
public get followups(): string[] | undefined {
return this._followups;
}
constructor(public response: IMarkdownString, isComplete: boolean = false, followups?: string[]) {
super();
this._isComplete = isComplete;
this._followups = followups;
this._id = 'response_' + InteractiveResponseModel.nextId++;
}
updateContent(responsePart: string) {
this.response = new MarkdownString(this.response.value + responsePart);
this._onDidChange.fire();
}
complete(followups: string[] | undefined): void {
this._isComplete = true;
this._followups = followups;
this._onDidChange.fire();
}
}
export interface IInteractiveSessionModel {
readonly onDidDispose: Event<void>;
readonly onDidChange: Event<IInteractiveSessionChangeEvent>;
readonly sessionId: number;
getRequests(): IInteractiveRequestModel[];
}
export interface IDeserializedInteractiveSessionData {
requests: InteractiveRequestModel[];
providerState: any;
}
export interface ISerializableInteractiveSessionData {
requests: { message: string; response: string | undefined }[];
providerState: any;
}
export type IInteractiveSessionChangeEvent = IInteractiveSessionAddRequestEvent | IInteractiveSessionAddResponseEvent | IInteractiveSessionClearEvent;
export interface IInteractiveSessionAddRequestEvent {
kind: 'addRequest';
request: IInteractiveRequestModel;
}
export interface IInteractiveSessionAddResponseEvent {
kind: 'addResponse';
response: IInteractiveResponseModel;
}
export interface IInteractiveSessionClearEvent {
kind: 'clear';
}
export class InteractiveSessionModel extends Disposable implements IInteractiveSessionModel {
private readonly _onDidDispose = this._register(new Emitter<void>());
readonly onDidDispose = this._onDidDispose.event;
private readonly _onDidChange = this._register(new Emitter<IInteractiveSessionChangeEvent>());
readonly onDidChange = this._onDidChange.event;
private _requests: InteractiveRequestModel[];
private _providerState: any;
static deserialize(obj: ISerializableInteractiveSessionData): IDeserializedInteractiveSessionData {
const requests = obj.requests;
if (!Array.isArray(requests)) {
throw new Error(`Malformed session data: ${obj}`);
}
const requestModels = requests.map((r: any) => {
const request = new InteractiveRequestModel(r.message);
if (r.response) {
request.response = new InteractiveResponseModel(new MarkdownString(r.response), true);
}
return request;
});
return { requests: requestModels, providerState: obj.providerState };
}
get sessionId(): number {
return this.session.id;
}
constructor(public readonly session: IInteractiveSession, public readonly providerId: string, initialData?: IDeserializedInteractiveSessionData) {
super();
this._requests = initialData ? initialData.requests : [];
this._providerState = initialData ? initialData.providerState : undefined;
}
acceptNewProviderState(providerState: any): void {
this._providerState = providerState;
}
clear(): void {
this._requests.forEach(r => r.response?.dispose());
this._requests = [];
this._onDidChange.fire({ kind: 'clear' });
}
getRequests(): InteractiveRequestModel[] {
return this._requests;
}
addRequest(request: InteractiveRequestModel): void {
// TODO this is suspicious, maybe the request should know that it is "in progress" instead of having a fake response model.
// But the response already knows that it is "in progress" and so does a map in the session service.
request.response = new InteractiveResponseModel(new MarkdownString(''));
this._requests.push(request);
this._onDidChange.fire({ kind: 'addRequest', request });
}
mergeResponseContent(request: InteractiveRequestModel, part: string): void {
if (request.response) {
request.response.updateContent(part);
} else {
request.response = new InteractiveResponseModel(new MarkdownString(part));
}
}
completeResponse(request: InteractiveRequestModel, followups?: string[]): void {
request.response!.complete(followups);
}
setResponse(request: InteractiveRequestModel, response: InteractiveResponseModel): void {
request.response = response;
this._onDidChange.fire({ kind: 'addResponse', response });
}
toJSON(): ISerializableInteractiveSessionData {
return {
requests: this._requests.map(r => {
return {
message: r.message,
response: r.response ? r.response.response.value : undefined,
};
}),
providerState: this._providerState
};
}
override dispose() {
this._requests.forEach(r => r.response?.dispose());
this._onDidDispose.fire();
super.dispose();
}
}

View file

@ -0,0 +1,55 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CancellationToken } from 'vs/base/common/cancellation';
import { IDisposable } from 'vs/base/common/lifecycle';
import { ProviderResult } from 'vs/editor/common/languages';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { InteractiveSessionModel } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionModel';
export interface IInteractiveSession {
id: number;
dispose?(): void;
}
export interface IInteractiveRequest {
session: IInteractiveSession;
message: string;
}
export interface IInteractiveResponse {
session: IInteractiveSession;
followups?: string[];
}
export interface IInteractiveProgress {
responsePart: string;
}
export interface IPersistedInteractiveState { }
export interface IInteractiveProvider {
id: string;
prepareSession(initialState: IPersistedInteractiveState | undefined, token: CancellationToken): ProviderResult<IInteractiveSession | undefined>;
resolveRequest?(session: IInteractiveSession, context: any, token: CancellationToken): ProviderResult<IInteractiveRequest>;
provideSuggestions(token: CancellationToken): ProviderResult<string[] | undefined>;
provideReply(request: IInteractiveRequest, progress: (progress: IInteractiveProgress) => void, token: CancellationToken): ProviderResult<IInteractiveResponse>;
}
export const IInteractiveSessionService = createDecorator<IInteractiveSessionService>('IInteractiveSessionService');
export interface IInteractiveSessionService {
_serviceBrand: undefined;
registerProvider(provider: IInteractiveProvider): IDisposable;
startSession(providerId: string, allowRestoringSession: boolean, token: CancellationToken): Promise<InteractiveSessionModel | undefined>;
/**
* Returns whether the request was accepted.
*/
sendRequest(sessionId: number, message: string, token: CancellationToken): boolean;
clearSession(sessionId: number): void;
acceptNewSessionState(sessionId: number, state: any): void;
addInteractiveRequest(context: any): void;
provideSuggestions(providerId: string, token: CancellationToken): Promise<string[] | undefined>;
}

View file

@ -0,0 +1,222 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CancellationToken } from 'vs/base/common/cancellation';
import { Iterable } from 'vs/base/common/iterator';
import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { withNullAsUndefined } from 'vs/base/common/types';
import { ILogService } from 'vs/platform/log/common/log';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { InteractiveRequestModel, InteractiveSessionModel, IDeserializedInteractiveSessionData } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionModel';
import { IInteractiveProgress, IInteractiveProvider, IInteractiveSessionService } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionService';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
const serializedInteractiveSessionKey = 'interactive.sessions';
export class InteractiveSessionService extends Disposable implements IInteractiveSessionService {
declare _serviceBrand: undefined;
private readonly _providers = new Map<string, IInteractiveProvider>();
private readonly _sessionModels = new Map<number, InteractiveSessionModel>();
private readonly _pendingRequestSessions = new Set<number>();
private readonly _unprocessedPersistedSessions: IDeserializedInteractiveSessionData[];
constructor(
@IStorageService storageService: IStorageService,
@ILogService private readonly logService: ILogService,
@IExtensionService private readonly extensionService: IExtensionService
) {
super();
const sessionData = storageService.get(serializedInteractiveSessionKey, StorageScope.WORKSPACE, '');
if (sessionData) {
this._unprocessedPersistedSessions = this.restoreInteractiveSessions(sessionData);
this.trace('constructor', `Restored ${this._unprocessedPersistedSessions.length} persisted sessions`);
} else {
this._unprocessedPersistedSessions = [];
this.trace('constructor', 'No persisted sessions');
}
this._register(storageService.onWillSaveState(e => {
const serialized = JSON.stringify(Array.from(this._sessionModels.values()));
this.trace('onWillSaveState', `Persisting ${this._sessionModels.size} sessions`);
storageService.store(serializedInteractiveSessionKey, serialized, StorageScope.WORKSPACE, StorageTarget.MACHINE);
}));
}
private trace(method: string, message: string): void {
this.logService.trace(`[InteractiveSessionService#${method}] ${message}`);
}
private error(method: string, message: string): void {
this.logService.error(`[InteractiveSessionService#${method}] ${message}`);
}
private restoreInteractiveSessions(sessionData: string): IDeserializedInteractiveSessionData[] {
try {
const obj = JSON.parse(sessionData);
if (!Array.isArray(obj)) {
throw new Error('Expected array');
}
return obj.map(item => InteractiveSessionModel.deserialize(item));
} catch (err) {
this.error('restoreInteractiveSessions', `Malformed session data: ${err}. [${sessionData.substring(0, 20)}...]`);
return [];
}
}
async startSession(providerId: string, allowRestoringSession: boolean, token: CancellationToken): Promise<InteractiveSessionModel | undefined> {
this.trace('startSession', `providerId=${providerId}, allowRestoringSession=${allowRestoringSession}`);
await this.extensionService.activateByEvent(`onInteractiveSession:${providerId}`);
const provider = this._providers.get(providerId);
if (!provider) {
throw new Error(`Unknown provider: ${providerId}`);
}
const someSessionHistory = allowRestoringSession ? this._unprocessedPersistedSessions.shift() : undefined;
this.trace('startSession', `Has history: ${!!someSessionHistory}. Including provider state: ${!!someSessionHistory?.providerState}`);
const session = await provider.prepareSession(someSessionHistory?.providerState, token);
if (!session) {
if (someSessionHistory) {
this._unprocessedPersistedSessions.unshift(someSessionHistory);
}
this.trace('startSession', 'Provider returned no session');
return undefined;
}
this.trace('startSession', `Provider returned session with id ${session.id}`);
const model = new InteractiveSessionModel(session, providerId, someSessionHistory);
this._sessionModels.set(model.sessionId, model);
return model;
}
sendRequest(sessionId: number, message: string, token: CancellationToken): boolean {
this.trace('sendRequest', `sessionId: ${sessionId}, message: ${message.substring(0, 20)}[...]`);
if (!message.trim()) {
this.trace('sendRequest', 'Rejected empty message');
return false;
}
const model = this._sessionModels.get(sessionId);
if (!model) {
throw new Error(`Unknown session: ${sessionId}`);
}
const provider = this._providers.get(model.providerId);
if (!provider) {
throw new Error(`Unknown provider: ${model.providerId}`);
}
if (this._pendingRequestSessions.has(sessionId)) {
this.trace('sendRequest', `Session ${sessionId} already has a pending request`);
return false;
}
// TODO log failures, add dummy response with error message
const _sendRequest = async (): Promise<void> => {
try {
this._pendingRequestSessions.add(sessionId);
const request = new InteractiveRequestModel(message);
model.addRequest(request);
const progressCallback = (progress: IInteractiveProgress) => {
this.trace('sendRequest', `Provider returned progress for session ${sessionId}, ${progress.responsePart.length} chars`);
model.mergeResponseContent(request, progress.responsePart);
};
const rawResponse = await provider.provideReply({ session: model.session, message }, progressCallback, token);
if (!rawResponse) {
this.trace('sendRequest', `Provider returned no response for session ${sessionId}`);
return;
}
model.completeResponse(request, rawResponse.followups);
this.trace('sendRequest', `Provider returned response for session ${sessionId} with ${rawResponse.followups} followups`);
} finally {
this._pendingRequestSessions.delete(sessionId);
}
};
// Return immediately that the request was accepted, don't wait
_sendRequest();
return true;
}
acceptNewSessionState(sessionId: number, state: any): void {
this.trace('acceptNewSessionState', `sessionId: ${sessionId}`);
const model = this._sessionModels.get(sessionId);
if (!model) {
throw new Error(`Unknown session: ${sessionId}`);
}
model.acceptNewProviderState(state);
}
async addInteractiveRequest(context: any): Promise<void> {
// TODO How to decide which session this goes to?
const model = Iterable.first(this._sessionModels.values());
if (!model) {
// If no session, create one- how and is the service the right place to decide this?
this.trace('addInteractiveRequest', 'No session available');
return;
}
const provider = this._providers.get(model.providerId);
if (!provider || !provider.resolveRequest) {
this.trace('addInteractiveRequest', 'No provider available');
return undefined;
}
this.trace('addInteractiveRequest', `Calling resolveRequest for session ${model.sessionId}`);
const request = await provider.resolveRequest(model.session, context, CancellationToken.None);
if (!request) {
this.trace('addInteractiveRequest', `Provider returned no request for session ${model.sessionId}`);
return;
}
// Maybe this API should queue a request after the current one?
this.trace('addInteractiveRequest', `Sending resolved request for session ${model.sessionId}`);
this.sendRequest(model.sessionId, request.message, CancellationToken.None);
}
clearSession(sessionId: number): void {
this.trace('clearSession', `sessionId: ${sessionId}`);
const model = this._sessionModels.get(sessionId);
if (!model) {
throw new Error(`Unknown session: ${sessionId}`);
}
model.dispose();
this._sessionModels.delete(sessionId);
}
registerProvider(provider: IInteractiveProvider): IDisposable {
this.trace('registerProvider', `Adding new interactive session provider`);
this._providers.set(provider.id, provider);
return toDisposable(() => {
this.trace('registerProvider', `Disposing interactive session provider`);
this._providers.delete(provider.id);
});
}
getAll() {
return [...this._providers];
}
async provideSuggestions(providerId: string, token: CancellationToken): Promise<string[] | undefined> {
await this.extensionService.activateByEvent(`onInteractiveSession:${providerId}`);
const provider = this._providers.get(providerId);
if (!provider) {
throw new Error(`Unknown provider: ${providerId}`);
}
const suggestions = await provider.provideSuggestions(token);
this.trace('provideSuggestions', `Provider returned ${suggestions?.length} suggestions`);
return withNullAsUndefined(suggestions);
}
}

View file

@ -0,0 +1,172 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Emitter, Event } from 'vs/base/common/event';
import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent';
import { Disposable } from 'vs/base/common/lifecycle';
import { IInteractiveRequestModel, IInteractiveResponseModel, IInteractiveSessionModel } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionModel';
export function isRequestVM(item: unknown): item is IInteractiveRequestViewModel {
return !!item && typeof (item as IInteractiveRequestViewModel).model !== 'undefined';
}
export function isResponseVM(item: unknown): item is IInteractiveResponseViewModel {
return !isRequestVM(item);
}
export interface IInteractiveSessionViewModel {
sessionId: number;
onDidDispose: Event<void>;
onDidChange: Event<void>;
getItems(): (IInteractiveRequestViewModel | IInteractiveResponseViewModel)[];
}
export interface IInteractiveRequestViewModel {
readonly id: string;
readonly model: IInteractiveRequestModel;
currentRenderedHeight: number | undefined;
}
export interface IInteractiveResponseRenderData {
renderPosition: number;
renderTime: number;
isFullyRendered: boolean;
}
export interface IInteractiveResponseViewModel {
readonly onDidChange: Event<void>;
readonly id: string;
readonly response: IMarkdownString;
readonly isComplete: boolean;
readonly followups?: string[];
renderData?: IInteractiveResponseRenderData;
currentRenderedHeight: number | undefined;
}
export class InteractiveSessionViewModel extends Disposable {
private readonly _onDidDispose = this._register(new Emitter<void>());
readonly onDidDispose = this._onDidDispose.event;
private readonly _onDidChange = this._register(new Emitter<void>());
readonly onDidChange = this._onDidChange.event;
private readonly _items: (IInteractiveRequestViewModel | IInteractiveResponseViewModel)[] = [];
get sessionId() {
return this.model.sessionId;
}
constructor(private readonly model: IInteractiveSessionModel) {
super();
model.getRequests().forEach((request, i) => {
this._items.push(new InteractiveRequestViewModel(request));
if (request.response) {
this._items.push(new InteractiveResponseViewModel(request.response));
}
});
this._register(model.onDidDispose(() => this._onDidDispose.fire()));
this._register(model.onDidChange(e => {
if (e.kind === 'clear') {
this._items.length = 0;
this._onDidChange.fire();
} else if (e.kind === 'addRequest') {
this._items.push(new InteractiveRequestViewModel(e.request));
if (e.request.response) {
this.onAddResponse(e.request.response);
}
} else if (e.kind === 'addResponse') {
this.onAddResponse(e.response);
}
this._onDidChange.fire();
}));
}
private onAddResponse(responseModel: IInteractiveResponseModel) {
const response = new InteractiveResponseViewModel(responseModel);
this._register(response.onDidChange(() => this._onDidChange.fire()));
this._items.push(response);
}
getItems() {
return this._items;
}
override dispose() {
super.dispose();
this._items
.filter((item): item is InteractiveResponseViewModel => item instanceof InteractiveResponseViewModel)
.forEach((item: InteractiveResponseViewModel) => item.dispose());
}
}
export class InteractiveRequestViewModel implements IInteractiveRequestViewModel {
get id() {
return this.model.id;
}
currentRenderedHeight: number | undefined;
constructor(readonly model: IInteractiveRequestModel) { }
}
export class InteractiveResponseViewModel extends Disposable implements IInteractiveResponseViewModel {
private _changeCount = 0;
private readonly _onDidChange = this._register(new Emitter<void>());
readonly onDidChange = this._onDidChange.event;
private _isPlaceholder = false;
get id() {
return this._model.id + `_${this._changeCount}`;
}
get response(): IMarkdownString {
if (this._isPlaceholder) {
return new MarkdownString('Thinking...');
}
return this._model.response;
}
get isComplete() {
return this._model.isComplete;
}
get followups() {
return this._model.followups;
}
renderData: IInteractiveResponseRenderData | undefined = undefined;
currentRenderedHeight: number | undefined;
constructor(private readonly _model: IInteractiveResponseModel) {
super();
this._isPlaceholder = !_model.response.value && !_model.isComplete;
this._register(_model.onDidChange(() => {
if (this._isPlaceholder && _model.response.value) {
this._isPlaceholder = false;
if (this.renderData) {
this.renderData.renderPosition = 0;
}
}
// new data -> new id, new content to render
this._changeCount++;
if (this.renderData) {
this.renderData.isFullyRendered = false;
this.renderData.renderTime = Date.now();
}
this._onDidChange.fire();
}));
}
}

View file

@ -202,7 +202,12 @@ export const tocData: ITOCEntry<string> = {
id: 'features/mergeEditor',
label: localize('mergeEditor', 'Merge Editor'),
settings: ['mergeEditor.*']
}
},
{
id: 'features/interactiveSession',
label: localize('interactiveSession', 'Interactive Session'),
settings: ['interactiveSession.*']
},
]
},
{

View file

@ -38,6 +38,7 @@ export const allApiProposals = Object.freeze({
idToken: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.idToken.d.ts',
indentSize: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.indentSize.d.ts',
inlineCompletionsAdditions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts',
interactive: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.interactive.d.ts',
interactiveWindow: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.interactiveWindow.d.ts',
ipc: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.ipc.d.ts',
notebookCellExecutionState: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookCellExecutionState.d.ts',

View file

@ -168,6 +168,8 @@ import 'vs/workbench/contrib/contextmenu/browser/contextmenu.contribution';
// Notebook
import 'vs/workbench/contrib/notebook/browser/notebook.contribution';
import 'vs/workbench/contrib/interactiveSession/browser/interactiveSession.contribution';
// Interactive
import 'vs/workbench/contrib/interactive/browser/interactive.contribution';

View file

@ -0,0 +1,89 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
declare module 'vscode' {
// todo@API make classes
export interface InteractiveEditorSession {
placeholder?: string;
}
// todo@API make classes
export interface InteractiveEditorRequest {
session: InteractiveEditorSession;
prompt: string;
selection: Selection;
wholeRange: Range;
}
// todo@API make classes
export interface InteractiveEditorResponse {
edits: TextEdit[];
placeholder?: string;
}
export interface TextDocumentContext {
document: TextDocument;
selection: Selection;
action?: string;
}
export interface InteractivEditorSessionProvider {
// Create a session. The lifetime of this session is the duration of the editing session with the input mode widget.
prepareInteractiveEditorSession(context: TextDocumentContext, token: CancellationToken): ProviderResult<InteractiveEditorSession>;
provideInteractivEditorResponse(request: InteractiveEditorRequest, token: CancellationToken): ProviderResult<InteractiveEditorResponse>;
// eslint-disable-next-line local/vscode-dts-provider-naming
releaseInteractiveEditorSession?(session: InteractiveEditorSession): any;
}
export interface InteractiveSessionState { }
export interface InteractiveSession {
saveState?(): InteractiveSessionState;
}
export interface InteractiveSessionRequestArgs {
command: string;
args: any;
}
export interface InteractiveRequest {
session: InteractiveSession;
message: string;
}
export interface InteractiveResponse {
content: string;
followups?: string[];
}
export interface InteractiveResponseForProgress {
followups?: string[];
}
export interface InteractiveProgress {
content: string;
}
export interface InteractiveSessionProvider {
provideInitialSuggestions?(token: CancellationToken): ProviderResult<string[]>;
prepareSession(initialState: InteractiveSessionState | undefined, token: CancellationToken): ProviderResult<InteractiveSession>;
resolveRequest(session: InteractiveSession, context: InteractiveSessionRequestArgs | string, token: CancellationToken): ProviderResult<InteractiveRequest>;
provideResponse?(request: InteractiveRequest, token: CancellationToken): ProviderResult<InteractiveResponse>;
provideResponseWithProgress?(request: InteractiveRequest, progress: Progress<InteractiveProgress>, token: CancellationToken): ProviderResult<InteractiveResponseForProgress>;
}
export namespace interactive {
// current version of the proposal.
export const _version: 1 | number;
export function registerInteractiveSessionProvider(id: string, provider: InteractiveSessionProvider): Disposable;
export function addInteractiveRequest(context: InteractiveSessionRequestArgs): void;
}
}