Skip to content

Commit e656ed6

Browse files
committed
update tests and code
1 parent 30b5261 commit e656ed6

5 files changed

Lines changed: 499 additions & 13 deletions

File tree

src/autocomplete/CompletionUtils.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,17 +139,19 @@ export function createCompletionItem(
139139
* @param params The completion parameters
140140
* @param documentManager The document manager to get line content
141141
* @param loggerName The name of the logger for warning messages
142+
* @param filterText The validated text clients should use to filter this completion
142143
*/
143144
export function handleSnippetJsonQuotes(
144145
completionItem: ExtendedCompletionItem,
145146
context: Context,
146147
params: CompletionParams,
147148
documentManager: DocumentManager,
148149
loggerName: string,
150+
filterText: string = context.text,
149151
): void {
150152
const log = LoggerFactory.getLogger(loggerName);
151153
const uri = params.textDocument.uri;
152-
const lineContent = documentManager.getLine(uri, context.startPosition.row);
154+
const lineContent = documentManager.getLine(uri, params.position.line);
153155

154156
const hasQuotes = lineContent?.includes('"');
155157

@@ -161,7 +163,7 @@ export function handleSnippetJsonQuotes(
161163
range.start.character += 1;
162164
}
163165
completionItem.textEdit = TextEdit.replace(range, `${completionItem.insertText}`);
164-
completionItem.filterText = `"${context.text}"`;
166+
completionItem.filterText = `"${filterText}"`;
165167
delete completionItem.insertText;
166168
} else {
167169
log.warn(

src/autocomplete/TopLevelSectionCompletionProvider.ts

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { DocumentManager } from '../document/DocumentManager';
77
import { FeatureFlag } from '../featureFlag/FeatureFlagI';
88
import { LoggerFactory } from '../telemetry/LoggerFactory';
99
import { Measure } from '../telemetry/TelemetryDecorator';
10-
import { getFuzzySearchFunction } from '../utils/FuzzySearchUtil';
10+
import { getFuzzySearchFunction, MAX_FUZZY_QUERY_LENGTH } from '../utils/FuzzySearchUtil';
1111
import { applySnippetIndentation } from '../utils/IndentationUtils';
1212
import { CompletionFormatter, ExtendedCompletionItem } from './CompletionFormatter';
1313
import { CompletionProvider } from './CompletionProvider';
@@ -89,9 +89,14 @@ ${CompletionFormatter.getIndentPlaceholder(1)}\${1:ConditionName}: $2`,
8989

9090
@Measure({ name: 'getCompletions' })
9191
getCompletions(context: Context, params: CompletionParams): CompletionItem[] | undefined {
92+
const { query, isComment } = this.resolveSectionQuery(context, params);
93+
if (isComment) {
94+
return [];
95+
}
96+
9297
// Get both regular and snippet completions
9398
const stringCompletions = this.getTopLevelSectionCompletions();
94-
const snippetCompletions = this.getTopLevelSectionSnippetCompletions(context, params);
99+
const snippetCompletions = this.getTopLevelSectionSnippetCompletions(context, params, query);
95100

96101
// Combine both types of completions
97102
let completions = [...stringCompletions, ...snippetCompletions];
@@ -106,13 +111,35 @@ ${CompletionFormatter.getIndentPlaceholder(1)}\${1:ConditionName}: $2`,
106111
});
107112
}
108113

109-
if (context.text?.length > 0) {
110-
return this.sectionKeywordFs(completions, context.text);
114+
if (query.length > 0) {
115+
return this.sectionKeywordFs(completions, query);
111116
}
112117

113118
return completions;
114119
}
115120

121+
// In malformed templates, Context.text can be an entire syntax node rather than the token at the cursor.
122+
private resolveSectionQuery(context: Context, params: CompletionParams): { query: string; isComment: boolean } {
123+
const line = this.documentManager.getLine(params.textDocument.uri, params.position.line);
124+
if (line !== undefined) {
125+
const beforeCursor = line.slice(0, params.position.character);
126+
const cursorToken = beforeCursor.match(/[A-Za-z]+$/)?.[0] ?? '';
127+
return {
128+
query: this.isValidSectionQuery(cursorToken) ? cursorToken : '',
129+
isComment: context.documentType === DocumentType.YAML && beforeCursor.trimStart().startsWith('#'),
130+
};
131+
}
132+
133+
return {
134+
query: this.isValidSectionQuery(context.text) ? context.text : '',
135+
isComment: context.documentType === DocumentType.YAML && context.text.trimStart().startsWith('#'),
136+
};
137+
}
138+
139+
private isValidSectionQuery(text: string): boolean {
140+
return text.length > 0 && text.length <= MAX_FUZZY_QUERY_LENGTH && /^[A-Za-z]+$/.test(text);
141+
}
142+
116143
private getTopLevelSectionCompletions(): CompletionItem[] {
117144
return TopLevelSections.filter((section) => {
118145
if (section === String(TopLevelSection.Constants)) {
@@ -132,7 +159,11 @@ ${CompletionFormatter.getIndentPlaceholder(1)}\${1:ConditionName}: $2`,
132159
});
133160
}
134161

135-
private getTopLevelSectionSnippetCompletions(context: Context, params: CompletionParams): CompletionItem[] {
162+
private getTopLevelSectionSnippetCompletions(
163+
context: Context,
164+
params: CompletionParams,
165+
query: string,
166+
): CompletionItem[] {
136167
const snippets: CompletionItem[] = [];
137168

138169
// Add snippets for top level sections
@@ -141,7 +172,7 @@ ${CompletionFormatter.getIndentPlaceholder(1)}\${1:ConditionName}: $2`,
141172
continue;
142173
}
143174

144-
snippets.push(this.createSectionSnippet(section as TopLevelSection, context, params));
175+
snippets.push(this.createSectionSnippet(section as TopLevelSection, context, params, query));
145176
}
146177

147178
return snippets;
@@ -151,6 +182,7 @@ ${CompletionFormatter.getIndentPlaceholder(1)}\${1:ConditionName}: $2`,
151182
section: TopLevelSection,
152183
context: Context,
153184
params: CompletionParams,
185+
query: string,
154186
): ExtendedCompletionItem {
155187
const snippetTemplate = this.sectionSnippets[section];
156188

@@ -170,7 +202,7 @@ ${CompletionFormatter.getIndentPlaceholder(1)}\${1:ConditionName}: $2`,
170202
});
171203

172204
completionItem.insertTextFormat = InsertTextFormat.Snippet;
173-
completionItem.filterText = context.text;
205+
completionItem.filterText = query;
174206

175207
// Handle JSON quotes if needed
176208
if (context.documentType === DocumentType.JSON) {
@@ -180,6 +212,7 @@ ${CompletionFormatter.getIndentPlaceholder(1)}\${1:ConditionName}: $2`,
180212
params,
181213
this.documentManager,
182214
TopLevelSectionCompletionProvider.name,
215+
query,
183216
);
184217
}
185218

tst/unit/autocomplete/TopLevelSectionCompletionProvider.test.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ describe('TopLevelSectionCompletionProvider', () => {
2727

2828
beforeEach(() => {
2929
mockComponents.syntaxTreeManager.getSyntaxTree.reset();
30+
mockDocumentManager.getLine.reset();
3031
mockSyntaxTree.topLevelSections.reset();
3132
mockComponents.syntaxTreeManager.getSyntaxTree.returns(mockSyntaxTree);
3233

@@ -530,4 +531,114 @@ describe('TopLevelSectionCompletionProvider', () => {
530531
expect(constantsItem).toBeUndefined();
531532
});
532533
});
534+
535+
describe('Cursor-local query resolution', () => {
536+
const wholeDocumentNodeText = Array.from({ length: 1200 }, () => ' SomeKey: SomeVeryLongValueToken').join(
537+
'\n',
538+
);
539+
540+
function paramsForLine(line: string): CompletionParams {
541+
return {
542+
textDocument: { uri: 'file:///test.yaml' },
543+
position: { line: 0, character: line.length },
544+
};
545+
}
546+
547+
test('ranks with the cursor token when parser context contains a large multiline node', () => {
548+
const cursorLine = 'Res';
549+
mockSyntaxTree.topLevelSections.returns([]);
550+
mockDocumentManager.getLine.returns(cursorLine);
551+
552+
const mockContext = createTopLevelContext('Unknown', { text: wholeDocumentNodeText });
553+
554+
const result = provider.getCompletions(mockContext, paramsForLine(cursorLine));
555+
556+
const resources = result?.find(
557+
(item) => item.label === 'Resources' && item.kind === CompletionItemKind.Class,
558+
);
559+
const resourcesSnippet = result?.find(
560+
(item) => item.label === 'Resources' && item.kind === CompletionItemKind.File,
561+
);
562+
expect(resources?.preselect).toBe(true);
563+
expect(resourcesSnippet?.filterText).toBe(cursorLine);
564+
});
565+
566+
test('ranks with the cursor token when parser context is empty', () => {
567+
const cursorLine = 'Par';
568+
mockSyntaxTree.topLevelSections.returns([]);
569+
mockDocumentManager.getLine.returns(cursorLine);
570+
571+
const mockContext = createTopLevelContext('Unknown', { text: '' });
572+
573+
const result = provider.getCompletions(mockContext, paramsForLine(cursorLine));
574+
575+
const parameters = result?.find(
576+
(item) => item.label === 'Parameters' && item.kind === CompletionItemKind.Class,
577+
);
578+
expect(parameters?.preselect).toBe(true);
579+
});
580+
581+
test('prefers the cursor token over stale short parser context', () => {
582+
const cursorLine = 'Res';
583+
mockSyntaxTree.topLevelSections.returns([]);
584+
mockDocumentManager.getLine.returns(cursorLine);
585+
586+
const mockContext = createTopLevelContext('Unknown', { text: 'Par' });
587+
588+
const result = provider.getCompletions(mockContext, paramsForLine(cursorLine));
589+
590+
const resources = result?.find(
591+
(item) => item.label === 'Resources' && item.kind === CompletionItemKind.Class,
592+
);
593+
expect(resources?.preselect).toBe(true);
594+
});
595+
596+
test('returns no completions when the cursor token is inside a YAML comment', () => {
597+
const cursorLine = ' # This mentions Conditions';
598+
mockSyntaxTree.topLevelSections.returns([]);
599+
mockDocumentManager.getLine.returns(cursorLine);
600+
601+
const mockContext = createTopLevelContext('Unknown', { text: '', type: DocumentType.YAML });
602+
603+
expect(provider.getCompletions(mockContext, paramsForLine(cursorLine))).toEqual([]);
604+
});
605+
606+
test('keeps JSON snippet filter text bounded after quote handling', () => {
607+
const cursorLine = ' "Res';
608+
mockSyntaxTree.topLevelSections.returns([]);
609+
mockDocumentManager.getLine.returns(cursorLine);
610+
611+
const mockContext = createTopLevelContext('Unknown', {
612+
text: wholeDocumentNodeText,
613+
type: DocumentType.JSON,
614+
});
615+
616+
const result = provider.getCompletions(mockContext, paramsForLine(cursorLine));
617+
618+
const resourcesSnippet = result?.find(
619+
(item) => item.label === 'Resources' && item.kind === CompletionItemKind.File,
620+
);
621+
expect(resourcesSnippet?.filterText).toBe('"Res"');
622+
expect(result?.every((item) => !item.filterText?.includes(wholeDocumentNodeText))).toBe(true);
623+
});
624+
625+
test('returns useful unranked completions when neither cursor nor context provides a valid query', () => {
626+
mockSyntaxTree.topLevelSections.returns([]);
627+
mockDocumentManager.getLine.returns(undefined);
628+
629+
const mockContext = createTopLevelContext('Unknown', { text: wholeDocumentNodeText });
630+
631+
const result = provider.getCompletions(mockContext, mockParams);
632+
633+
const resources = result?.find(
634+
(item) => item.label === 'Resources' && item.kind === CompletionItemKind.Class,
635+
);
636+
const resourcesSnippet = result?.find(
637+
(item) => item.label === 'Resources' && item.kind === CompletionItemKind.File,
638+
);
639+
expect(resources).toBeDefined();
640+
expect(resources?.preselect).toBeUndefined();
641+
expect(resourcesSnippet?.filterText).toBe('');
642+
});
643+
});
533644
});

0 commit comments

Comments
 (0)