diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index fa4c06d4b75..be4be58b6c5 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -797,6 +797,12 @@ export interface InlineCompletion { readonly range?: IRange; readonly command?: Command; + + /** + * If set to `true`, unopened closing brackets are removed and unclosed opening brackets are closed. + * Defaults to `false`. + */ + readonly completeBracketPairs?: boolean; } export interface InlineCompletions { diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index dd397ceb990..21e037446c3 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -938,6 +938,11 @@ export interface ITextModel { */ getTokenTypeIfInsertingCharacter(lineNumber: number, column: number, character: string): StandardTokenType; + /** + * @internal + */ + tokenizeLineWithEdit(position: IPosition, length: number, newText: string): LineTokens | null; + /** * Get the word under or besides `position`. * @param position The position to look for a word. diff --git a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/ast.ts b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/ast.ts index 822fbe33e8a..8e684e1e78a 100644 --- a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/ast.ts +++ b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/ast.ts @@ -5,7 +5,7 @@ import { CursorColumns } from 'vs/editor/common/core/cursorColumns'; import { ITextModel } from 'vs/editor/common/model'; -import { Length, lengthAdd, lengthGetLineCount, lengthHash, lengthToObj, lengthZero } from './length'; +import { Length, lengthAdd, lengthGetLineCount, lengthToObj, lengthZero } from './length'; import { SmallImmutableSet } from './smallImmutableSet'; import { OpeningBracketId } from './tokenizer'; @@ -624,17 +624,12 @@ export class TextAstNode extends ImmutableLeafAstNode { } export class BracketAstNode extends ImmutableLeafAstNode { - private static cacheByLength = new Map(); - - public static create(length: Length): BracketAstNode { - const lengthKey = lengthHash(length); - const cached = BracketAstNode.cacheByLength.get(lengthKey); - if (cached) { - return cached; - } - - const node = new BracketAstNode(length); - BracketAstNode.cacheByLength.set(lengthKey, node); + public static create( + length: Length, + languageId: string, + bracketIds: SmallImmutableSet + ): BracketAstNode { + const node = new BracketAstNode(length, languageId, bracketIds); return node; } @@ -646,7 +641,15 @@ export class BracketAstNode extends ImmutableLeafAstNode { return SmallImmutableSet.getEmpty(); } - private constructor(length: Length) { + private constructor( + length: Length, + public readonly languageId: string, + /** + * In case of a opening bracket, this is the id of the opening bracket. + * In case of a closing bracket, this contains the ids of all opening brackets it can close. + */ + public readonly bracketIds: SmallImmutableSet + ) { super(length); } diff --git a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/brackets.ts b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/brackets.ts index c650d6f7264..a81c4c47aa6 100644 --- a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/brackets.ts +++ b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/brackets.ts @@ -41,19 +41,20 @@ export class BracketTokens { TokenKind.ClosingBracket, info.first, info.openingBrackets, - BracketAstNode.create(length) + BracketAstNode.create(length, configuration.languageId, info.openingBrackets) )); } for (const openingText of openingBrackets) { const length = toLength(0, openingText.length); const openingTextId = getId(configuration.languageId, openingText); + const bracketIds = SmallImmutableSet.getEmpty().add(openingTextId, identityKeyProvider); map.set(openingText, new Token( length, TokenKind.OpeningBracket, openingTextId, - SmallImmutableSet.getEmpty().add(openingTextId, identityKeyProvider), - BracketAstNode.create(length) + bracketIds, + BracketAstNode.create(length, configuration.languageId, bracketIds) )); } @@ -94,6 +95,15 @@ export class BracketTokens { return this.map.get(value); } + findClosingTokenText(openingBracketIds: SmallImmutableSet): string | undefined { + for (const [closingText, info] of this.map) { + if (info.bracketIds.intersects(openingBracketIds)) { + return closingText; + } + } + return undefined; + } + get isEmpty(): boolean { return this.map.size === 0; } diff --git a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/tokenizer.ts b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/tokenizer.ts index 83f1bc6b553..0c572140a2d 100644 --- a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/tokenizer.ts +++ b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/tokenizer.ts @@ -4,13 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import { NotSupportedError } from 'vs/base/common/errors'; -import { LineTokens } from 'vs/editor/common/tokens/lineTokens'; -import { ITextModel } from 'vs/editor/common/model'; -import { SmallImmutableSet } from './smallImmutableSet'; import { StandardTokenType, TokenMetadata } from 'vs/editor/common/languages'; +import { IViewLineTokens } from 'vs/editor/common/tokens/lineTokens'; import { BracketAstNode, TextAstNode } from './ast'; import { BracketTokens, LanguageAgnosticBracketTokens } from './brackets'; -import { lengthGetColumnCountIfZeroLineCount, Length, lengthAdd, lengthDiff, lengthToObj, lengthZero, toLength } from './length'; +import { Length, lengthAdd, lengthDiff, lengthGetColumnCountIfZeroLineCount, lengthToObj, lengthZero, toLength } from './length'; +import { SmallImmutableSet } from './smallImmutableSet'; export interface Tokenizer { readonly offset: Length; @@ -51,6 +50,13 @@ export class Token { ) { } } +export interface ITokenizerSource { + getValue(): string; + getLineCount(): number; + getLineLength(lineNumber: number): number; + getLineTokens(lineNumber: number): IViewLineTokens; +} + export class TextBufferTokenizer implements Tokenizer { private readonly textBufferLineCount: number; private readonly textBufferLastLineLength: number; @@ -58,7 +64,7 @@ export class TextBufferTokenizer implements Tokenizer { private readonly reader = new NonPeekableTextBufferTokenizer(this.textModel, this.bracketTokens); constructor( - private readonly textModel: ITextModel, + private readonly textModel: ITokenizerSource, private readonly bracketTokens: LanguageAgnosticBracketTokens ) { this.textBufferLineCount = textModel.getLineCount(); @@ -119,7 +125,7 @@ class NonPeekableTextBufferTokenizer { private readonly textBufferLineCount: number; private readonly textBufferLastLineLength: number; - constructor(private readonly textModel: ITextModel, private readonly bracketTokens: LanguageAgnosticBracketTokens) { + constructor(private readonly textModel: ITokenizerSource, private readonly bracketTokens: LanguageAgnosticBracketTokens) { this.textBufferLineCount = textModel.getLineCount(); this.textBufferLastLineLength = textModel.getLineLength(this.textBufferLineCount); } @@ -127,7 +133,7 @@ class NonPeekableTextBufferTokenizer { private lineIdx = 0; private line: string | null = null; private lineCharOffset = 0; - private lineTokens: LineTokens | null = null; + private lineTokens: IViewLineTokens | null = null; private lineTokenOffset = 0; public setPosition(lineIdx: number, column: number): void { diff --git a/src/vs/editor/common/model/bracketPairsTextModelPart/fixBrackets.ts b/src/vs/editor/common/model/bracketPairsTextModelPart/fixBrackets.ts new file mode 100644 index 00000000000..866cd711a70 --- /dev/null +++ b/src/vs/editor/common/model/bracketPairsTextModelPart/fixBrackets.ts @@ -0,0 +1,82 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { AstNode, AstNodeKind } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/ast'; +import { LanguageAgnosticBracketTokens } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/brackets'; +import { Length, lengthAdd, lengthGetColumnCountIfZeroLineCount, lengthZero } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/length'; +import { parseDocument } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/parser'; +import { DenseKeyProvider } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/smallImmutableSet'; +import { ITokenizerSource, TextBufferTokenizer } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/tokenizer'; +import { IViewLineTokens } from 'vs/editor/common/tokens/lineTokens'; + +export function fixBracketsInLine(tokens: IViewLineTokens, languageConfigurationService: ILanguageConfigurationService): string { + const denseKeyProvider = new DenseKeyProvider(); + const bracketTokens = new LanguageAgnosticBracketTokens(denseKeyProvider, (languageId) => + languageConfigurationService.getLanguageConfiguration(languageId) + ); + const tokenizer = new TextBufferTokenizer( + new StaticTokenizerSource([tokens]), + bracketTokens + ); + const node = parseDocument(tokenizer, [], undefined, true); + + let str = ''; + const line = tokens.getLineContent(); + + function processNode(node: AstNode, offset: Length) { + if (node.kind === AstNodeKind.Pair) { + processNode(node.openingBracket, offset); + offset = lengthAdd(offset, node.openingBracket.length); + + if (node.child) { + processNode(node.child, offset); + offset = lengthAdd(offset, node.child.length); + } + if (node.closingBracket) { + processNode(node.closingBracket, offset); + offset = lengthAdd(offset, node.closingBracket.length); + } else { + const singleLangBracketTokens = bracketTokens.getSingleLanguageBracketTokens(node.openingBracket.languageId); + + const closingTokenText = singleLangBracketTokens.findClosingTokenText(node.openingBracket.bracketIds); + str += closingTokenText; + } + } else if (node.kind === AstNodeKind.UnexpectedClosingBracket) { + // remove the bracket + } else if (node.kind === AstNodeKind.Text || node.kind === AstNodeKind.Bracket) { + str += line.substring( + lengthGetColumnCountIfZeroLineCount(offset), + lengthGetColumnCountIfZeroLineCount(lengthAdd(offset, node.length)) + ); + } else if (node.kind === AstNodeKind.List) { + for (const child of node.children) { + processNode(child, offset); + offset = lengthAdd(offset, child.length); + } + } + } + + processNode(node, lengthZero); + + return str; +} + +class StaticTokenizerSource implements ITokenizerSource { + constructor(private readonly lines: IViewLineTokens[]) { } + + getValue(): string { + return this.lines.map(l => l.getLineContent()).join('\n'); + } + getLineCount(): number { + return this.lines.length; + } + getLineLength(lineNumber: number): number { + return this.lines[lineNumber - 1].getLineContent().length; + } + getLineTokens(lineNumber: number): IViewLineTokens { + return this.lines[lineNumber - 1]; + } +} diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 9ba323cdc71..d16ea630c00 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -2139,6 +2139,11 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati return this._tokenization.getTokenTypeIfInsertingCharacter(position, character); } + tokenizeLineWithEdit(position: IPosition, length: number, newText: string): LineTokens | null { + const validatedPosition = this.validatePosition(position); + return this._tokenization.tokenizeLineWithEdit(validatedPosition, length, newText); + } + private getLanguageConfiguration(languageId: string): ResolvedLanguageConfiguration { return this._languageConfigurationService.getLanguageConfiguration(languageId); } diff --git a/src/vs/editor/common/model/textModelTokens.ts b/src/vs/editor/common/model/textModelTokens.ts index 92a4d304360..5b84ecd6322 100644 --- a/src/vs/editor/common/model/textModelTokens.ts +++ b/src/vs/editor/common/model/textModelTokens.ts @@ -372,6 +372,38 @@ export class TextModelTokenization extends Disposable { return lineTokens.getStandardTokenType(tokenIndex); } + public tokenizeLineWithEdit(position: Position, length: number, newText: string): LineTokens | null { + const lineNumber = position.lineNumber; + const column = position.column; + + if (!this._tokenizationSupport) { + return null; + } + + this.forceTokenization(lineNumber); + const lineStartState = this._tokenizationStateStore.getBeginState(lineNumber - 1); + if (!lineStartState) { + return null; + } + + const curLineContent = this._textModel.getLineContent(lineNumber); + const newLineContent = curLineContent.substring(0, column - 1) + + newText + curLineContent.substring(column - 1 + length); + + const languageId = this._textModel.getLanguageIdAtPosition(lineNumber, 0); + const result = safeTokenize( + this._languageIdCodec, + languageId, + this._tokenizationSupport, + newLineContent, + true, + lineStartState + ); + + const lineTokens = new LineTokens(result.tokens, newLineContent, this._languageIdCodec); + return lineTokens; + } + public isCheapToTokenize(lineNumber: number): boolean { if (!this._tokenizationSupport) { return true; diff --git a/src/vs/editor/common/tokens/lineTokens.ts b/src/vs/editor/common/tokens/lineTokens.ts index 8d55228bbe3..4cf845fe710 100644 --- a/src/vs/editor/common/tokens/lineTokens.ts +++ b/src/vs/editor/common/tokens/lineTokens.ts @@ -15,6 +15,8 @@ export interface IViewLineTokens { getPresentation(tokenIndex: number): ITokenPresentation; findTokenIndexAtOffset(offset: number): number; getLineContent(): string; + getMetadata(tokenIndex: number): number; + getLanguageId(tokenIndex: number): string; } export class LineTokens implements IViewLineTokens { @@ -253,6 +255,14 @@ class SliceLineTokens implements IViewLineTokens { } } + public getMetadata(tokenIndex: number): number { + return this._source.getMetadata(this._firstTokenIndex + tokenIndex); + } + + public getLanguageId(tokenIndex: number): string { + return this._source.getLanguageId(this._firstTokenIndex + tokenIndex); + } + public getLineContent(): string { return this._source.getLineContent().substring(this._startOffset, this._endOffset); } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/ghostTextModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/ghostTextModel.ts index ce060a0e562..3f23c9a3e8e 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/ghostTextModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/ghostTextModel.ts @@ -9,8 +9,9 @@ import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { InlineCompletionTriggerKind } from 'vs/editor/common/languages'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { GhostText, GhostTextWidgetModel } from 'vs/editor/contrib/inlineCompletions/browser/ghostText'; -import { InlineCompletionsModel, LiveInlineCompletions, SynchronizedInlineCompletionsCache } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel'; +import { InlineCompletionsModel, SynchronizedInlineCompletionsCache, TrackedInlineCompletions } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel'; import { SuggestWidgetPreviewModel } from 'vs/editor/contrib/inlineCompletions/browser/suggestWidgetPreviewModel'; import { createDisposableRef } from 'vs/editor/contrib/inlineCompletions/browser/utils'; import { ICommandService } from 'vs/platform/commands/common/commands'; @@ -68,7 +69,7 @@ export abstract class DelegatingModel extends Disposable implements GhostTextWid export class GhostTextModel extends DelegatingModel implements GhostTextWidgetModel { public readonly sharedCache = this._register(new SharedInlineCompletionCache()); public readonly suggestWidgetAdapterModel = this._register(new SuggestWidgetPreviewModel(this.editor, this.sharedCache)); - public readonly inlineCompletionsModel = this._register(new InlineCompletionsModel(this.editor, this.sharedCache, this.commandService)); + public readonly inlineCompletionsModel = this._register(new InlineCompletionsModel(this.editor, this.sharedCache, this.commandService, this.languageConfigurationService)); public get activeInlineCompletionsModel(): InlineCompletionsModel | undefined { if (this.targetModel === this.inlineCompletionsModel) { @@ -79,7 +80,8 @@ export class GhostTextModel extends DelegatingModel implements GhostTextWidgetMo constructor( private readonly editor: IActiveCodeEditor, - @ICommandService private readonly commandService: ICommandService + @ICommandService private readonly commandService: ICommandService, + @ILanguageConfigurationService private readonly languageConfigurationService: ILanguageConfigurationService, ) { super(); @@ -143,7 +145,7 @@ export class SharedInlineCompletionCache extends Disposable { } public setValue(editor: IActiveCodeEditor, - completionsSource: LiveInlineCompletions, + completionsSource: TrackedInlineCompletions, triggerKind: InlineCompletionTriggerKind ) { this.cache.value = new SynchronizedInlineCompletionsCache( diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionToGhostText.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionToGhostText.ts index 0512605b0c3..8eeadc57098 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionToGhostText.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionToGhostText.ts @@ -11,6 +11,9 @@ import { ITextModel } from 'vs/editor/common/model'; import { InlineCompletion } from 'vs/editor/common/languages'; import { GhostText, GhostTextPart } from 'vs/editor/contrib/inlineCompletions/browser/ghostText'; +/** + * A normalized inline completion is an inline completion with a defined range. +*/ export interface NormalizedInlineCompletion extends InlineCompletion { range: Range; } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts index b12c47d368e..137829cf353 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts @@ -19,9 +19,11 @@ import { ITextModel } from 'vs/editor/common/model'; import { InlineCompletion, InlineCompletionContext, InlineCompletions, InlineCompletionsProvider, InlineCompletionsProviderRegistry, InlineCompletionTriggerKind } from 'vs/editor/common/languages'; import { BaseGhostTextWidgetModel, GhostText, GhostTextWidgetModel } from 'vs/editor/contrib/inlineCompletions/browser/ghostText'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { inlineSuggestCommitId } from './consts'; -import { SharedInlineCompletionCache } from './ghostTextModel'; -import { inlineCompletionToGhostText, NormalizedInlineCompletion } from './inlineCompletionToGhostText'; +import { inlineSuggestCommitId } from 'vs/editor/contrib/inlineCompletions/browser/consts'; +import { SharedInlineCompletionCache } from 'vs/editor/contrib/inlineCompletions/browser/ghostTextModel'; +import { inlineCompletionToGhostText, NormalizedInlineCompletion } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionToGhostText'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { fixBracketsInLine } from 'vs/editor/common/model/bracketPairsTextModelPart/fixBrackets'; export class InlineCompletionsModel extends Disposable implements GhostTextWidgetModel { protected readonly onDidChangeEmitter = new Emitter(); @@ -36,6 +38,7 @@ export class InlineCompletionsModel extends Disposable implements GhostTextWidge private readonly editor: IActiveCodeEditor, private readonly cache: SharedInlineCompletionCache, @ICommandService private readonly commandService: ICommandService, + @ILanguageConfigurationService private readonly languageConfigurationService: ILanguageConfigurationService, ) { super(); @@ -138,7 +141,8 @@ export class InlineCompletionsModel extends Disposable implements GhostTextWidge () => this.active, this.commandService, this.cache, - triggerKind + triggerKind, + this.languageConfigurationService ); this.completionSession.value.takeOwnership( this.completionSession.value.onDidChange(() => { @@ -189,7 +193,8 @@ export class InlineCompletionsSession extends BaseGhostTextWidgetModel { private readonly shouldUpdate: () => boolean, private readonly commandService: ICommandService, private readonly cache: SharedInlineCompletionCache, - private initialTriggerKind: InlineCompletionTriggerKind + private initialTriggerKind: InlineCompletionTriggerKind, + private readonly languageConfigurationService: ILanguageConfigurationService, ) { super(editor); @@ -310,7 +315,7 @@ export class InlineCompletionsSession extends BaseGhostTextWidgetModel { return currentCompletion ? inlineCompletionToGhostText(currentCompletion, this.editor.getModel(), mode, this.editor.getPosition()) : undefined; } - get currentCompletion(): LiveInlineCompletion | undefined { + get currentCompletion(): TrackedInlineCompletion | undefined { const completion = this.currentCachedCompletion; if (!completion) { return undefined; @@ -342,7 +347,8 @@ export class InlineCompletionsSession extends BaseGhostTextWidgetModel { result = await provideInlineCompletions(position, this.editor.getModel(), { triggerKind, selectedSuggestionInfo: undefined }, - token + token, + this.languageConfigurationService ); } catch (e) { onUnexpectedError(e); @@ -384,7 +390,7 @@ export class InlineCompletionsSession extends BaseGhostTextWidgetModel { } } - public commit(completion: LiveInlineCompletion): void { + public commit(completion: TrackedInlineCompletion): void { // Mark the cache as stale, but don't dispose it yet, // otherwise command args might get disposed. const cache = this.cache.clearAndLeak(); @@ -428,7 +434,7 @@ export class SynchronizedInlineCompletionsCache extends Disposable { constructor( editor: IActiveCodeEditor, - completionsSource: LiveInlineCompletions, + completionsSource: TrackedInlineCompletions, onChange: () => void, public readonly triggerKind: InlineCompletionTriggerKind, ) { @@ -486,13 +492,13 @@ class CachedInlineCompletion { public synchronizedRange: Range; constructor( - public readonly inlineCompletion: LiveInlineCompletion, + public readonly inlineCompletion: TrackedInlineCompletion, public readonly decorationId: string, ) { this.synchronizedRange = inlineCompletion.range; } - public toLiveInlineCompletion(): LiveInlineCompletion | undefined { + public toLiveInlineCompletion(): TrackedInlineCompletion | undefined { return { text: this.inlineCompletion.text, range: this.synchronizedRange, @@ -504,16 +510,29 @@ class CachedInlineCompletion { } } -export interface LiveInlineCompletion extends NormalizedInlineCompletion { +/** + * A normalized inline completion that tracks which inline completion it has been constructed from. +*/ +export interface TrackedInlineCompletion extends NormalizedInlineCompletion { sourceProvider: InlineCompletionsProvider; + + /** + * A reference to the original inline completion this inline completion has been constructed from. + * Used for event data to ensure referential equality. + */ sourceInlineCompletion: InlineCompletion; + + /** + * A reference to the original inline completion list this inline completion has been constructed from. + * Used for event data to ensure referential equality. + */ sourceInlineCompletions: InlineCompletions; } /** * Contains no duplicated items. */ -export interface LiveInlineCompletions extends InlineCompletions { +export interface TrackedInlineCompletions extends InlineCompletions { dispose(): void; } @@ -531,8 +550,9 @@ export async function provideInlineCompletions( position: Position, model: ITextModel, context: InlineCompletionContext, - token: CancellationToken = CancellationToken.None -): Promise { + token: CancellationToken = CancellationToken.None, + languageConfigurationService?: ILanguageConfigurationService +): Promise { const defaultReplaceRange = getDefaultRange(position, model); const providers = InlineCompletionsProviderRegistry.all(model); @@ -553,23 +573,38 @@ export async function provideInlineCompletions( ) ); - const itemsByHash = new Map(); + const itemsByHash = new Map(); for (const result of results) { const completions = result.completions; if (completions) { - for (const item of completions.items.map(item => ({ - text: item.text, - range: item.range ? Range.lift(item.range) : defaultReplaceRange, - command: item.command, - sourceProvider: result.provider, - sourceInlineCompletions: completions, - sourceInlineCompletion: item - }))) { - if (item.range.startLineNumber !== item.range.endLineNumber) { + for (const item of completions.items) { + const range = item.range ? Range.lift(item.range) : defaultReplaceRange; + + if (range.startLineNumber !== range.endLineNumber) { // Ignore invalid ranges. continue; } - itemsByHash.set(JSON.stringify({ text: item.text, range: item.range }), item); + + const text = + languageConfigurationService && item.completeBracketPairs + ? closeBrackets( + item.text, + range.getStartPosition(), + model, + languageConfigurationService + ) + : item.text; + + const trackedItem: TrackedInlineCompletion = ({ + text, + range, + command: item.command, + sourceProvider: result.provider, + sourceInlineCompletions: completions, + sourceInlineCompletion: item + }); + + itemsByHash.set(JSON.stringify({ text, range: item.range }), trackedItem); } } } @@ -584,6 +619,22 @@ export async function provideInlineCompletions( }; } +function closeBrackets(text: string, position: Position, model: ITextModel, languageConfigurationService: ILanguageConfigurationService): string { + const lineStart = model.getLineContent(position.lineNumber).substring(0, position.column - 1); + const newLine = lineStart + text; + + const newTokens = model.tokenizeLineWithEdit(position, newLine.length - (position.column - 1), text); + const slicedTokens = newTokens?.sliceAndInflate(position.column - 1, newLine.length, 0); + if (!slicedTokens) { + return text; + } + + console.log(slicedTokens); + const newText = fixBracketsInLine(slicedTokens, languageConfigurationService); + + return newText; +} + /** * Shrinks the range if the text has a suffix/prefix that agrees with the text buffer. * E.g. text buffer: `ab[cdef]ghi`, [...] is the replace range, `cxyzf` is the new text. diff --git a/src/vs/editor/test/common/core/testLineToken.ts b/src/vs/editor/test/common/core/testLineToken.ts index 3d277122399..0c6b236fbc7 100644 --- a/src/vs/editor/test/common/core/testLineToken.ts +++ b/src/vs/editor/test/common/core/testLineToken.ts @@ -107,6 +107,13 @@ export class TestLineTokens implements IViewLineTokens { throw new Error('Not implemented'); } + public getMetadata(tokenIndex: number): number { + throw new Error('Method not implemented.'); + } + + public getLanguageId(tokenIndex: number): string { + throw new Error('Method not implemented.'); + } } export class TestLineTokenFactory { diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index f805b764fdb..7810bc93b74 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -6191,6 +6191,11 @@ declare namespace monaco.languages { */ readonly range?: IRange; readonly command?: Command; + /** + * If set to `true`, unopened closing brackets are removed and unclosed opening brackets are closed. + * Defaults to `false`. + */ + readonly completeBracketPairs?: boolean; } export interface InlineCompletions { diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 0a9bf2e0ce9..5b1b6b649b4 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -1083,6 +1083,7 @@ class InlineCompletionAdapter { range: item.range ? typeConvert.Range.from(item.range) : undefined, command, idx: idx, + completeBracketPairs: item.completeBracketPairs }); }), }; diff --git a/src/vscode-dts/vscode.proposed.inlineCompletions.d.ts b/src/vscode-dts/vscode.proposed.inlineCompletions.d.ts index 7bae088dc27..df29423da81 100644 --- a/src/vscode-dts/vscode.proposed.inlineCompletions.d.ts +++ b/src/vscode-dts/vscode.proposed.inlineCompletions.d.ts @@ -103,6 +103,11 @@ declare module 'vscode' { */ command?: Command; + /** + * If set to `true`, unopened closing brackets are removed and unclosed opening brackets are closed. + * Defaults to `false`. + */ + completeBracketPairs?: boolean; constructor(text: string, range?: Range, command?: Command); }