mirror of
https://github.com/Microsoft/vscode
synced 2024-11-05 18:29:38 +00:00
Merge branch 'main' into betterFailureMessage
This commit is contained in:
commit
d989227967
30 changed files with 3178 additions and 13 deletions
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -51,7 +51,7 @@ class SelectionRanges {
|
|||
}
|
||||
}
|
||||
|
||||
class SmartSelectController implements IEditorContribution {
|
||||
export class SmartSelectController implements IEditorContribution {
|
||||
|
||||
static readonly ID = 'editor.contrib.smartSelectController';
|
||||
|
||||
|
|
|
@ -70,6 +70,7 @@ import './mainThreadNotebookKernels';
|
|||
import './mainThreadNotebookDocumentsAndEditors';
|
||||
import './mainThreadNotebookRenderers';
|
||||
import './mainThreadInteractive';
|
||||
import './mainThreadInteractiveSession';
|
||||
import './mainThreadTask';
|
||||
import './mainThreadLabelService';
|
||||
import './mainThreadTunnelService';
|
||||
|
|
102
src/vs/workbench/api/browser/mainThreadInteractiveSession.ts
Normal file
102
src/vs/workbench/api/browser/mainThreadInteractiveSession.ts
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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'),
|
||||
|
|
165
src/vs/workbench/api/common/extHostInteractiveSession.ts
Normal file
165
src/vs/workbench/api/common/extHostInteractiveSession.ts
Normal 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
|
||||
}
|
|
@ -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);
|
|
@ -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 } });
|
||||
}
|
||||
};
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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 });
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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.')
|
||||
);
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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>;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}));
|
||||
}
|
||||
}
|
|
@ -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.*']
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
89
src/vscode-dts/vscode.proposed.interactive.d.ts
vendored
Normal file
89
src/vscode-dts/vscode.proposed.interactive.d.ts
vendored
Normal 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;
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue