diff --git a/languageservice/src/complete.test.ts b/languageservice/src/complete.test.ts index 0798cde..1ba56a9 100644 --- a/languageservice/src/complete.test.ts +++ b/languageservice/src/complete.test.ts @@ -723,16 +723,28 @@ jobs: expect(switchToList!.sortText).toEqual("zzz_switch_1"); expect(switchToFull!.sortText).toEqual("zzz_switch_2"); - // Escape hatches should have textEdit that restructures the YAML + // Escape hatches should have textEdit at cursor position (for VS Code filtering compatibility) const listEdit = switchToList!.textEdit as TextEdit; const fullEdit = switchToFull!.textEdit as TextEdit; - expect(listEdit.newText).toEqual("runs-on:\n - "); - expect(fullEdit.newText).toEqual("runs-on:\n "); + // Main textEdit inserts newline and indented content at cursor position + expect(listEdit.newText).toEqual("\n - "); + expect(fullEdit.newText).toEqual("\n "); - // TextEdit range should cover from key start to cursor position - expect(listEdit.range.start).toEqual({line: 3, character: 4}); - expect(fullEdit.range.start).toEqual({line: 3, character: 4}); + // TextEdit range should be at cursor position (empty range) + expect(listEdit.range.start).toEqual({line: 3, character: 13}); + expect(listEdit.range.end).toEqual({line: 3, character: 13}); + expect(fullEdit.range.start).toEqual({line: 3, character: 13}); + expect(fullEdit.range.end).toEqual({line: 3, character: 13}); + + // additionalTextEdits should clean up the key portion + expect(switchToList!.additionalTextEdits).toHaveLength(1); + expect(switchToList!.additionalTextEdits![0].range.start).toEqual({line: 3, character: 4}); + expect(switchToList!.additionalTextEdits![0].range.end).toEqual({line: 3, character: 13}); + expect(switchToList!.additionalTextEdits![0].newText).toEqual("runs-on:"); + + expect(switchToFull!.additionalTextEdits).toHaveLength(1); + expect(switchToFull!.additionalTextEdits![0].newText).toEqual("runs-on:"); }); it("permissions shows only switch to full syntax (no sequence form)", async () => { @@ -824,9 +836,16 @@ jobs: const switchToList = result.find(x => x.label === "(switch to list)"); const textEdit = switchToList!.textEdit as TextEdit; + const additionalEdits = switchToList!.additionalTextEdits!; - // Applying this edit to "runs-on: " should produce "runs-on:\n - " - expect(textEdit.newText).toEqual("runs-on:\n - "); + // Main textEdit inserts newline content at cursor + expect(textEdit.newText).toEqual("\n - "); + + // additionalTextEdits replaces "runs-on: " with "runs-on:" + expect(additionalEdits).toHaveLength(1); + expect(additionalEdits[0].newText).toEqual("runs-on:"); + + // Combined result when applied: "runs-on:\n - " }); }); diff --git a/languageservice/src/complete.ts b/languageservice/src/complete.ts index eeabc76..720b9d6 100644 --- a/languageservice/src/complete.ts +++ b/languageservice/src/complete.ts @@ -192,6 +192,12 @@ export async function complete( textEdit = TextEdit.insert(position, newText); } + // Convert additionalTextEdits if present + let additionalTextEdits: TextEdit[] | undefined; + if (value.additionalTextEdits) { + additionalTextEdits = value.additionalTextEdits.map(edit => TextEdit.replace(edit.range, edit.newText)); + } + const item: CompletionItem = { label: value.label, labelDetails: value.labelDetail ? {description: value.labelDetail} : undefined, @@ -202,7 +208,8 @@ export async function complete( value: value.description }, tags: value.deprecated ? [CompletionItemTag.Deprecated] : undefined, - textEdit + textEdit, + additionalTextEdits }; return item; @@ -388,9 +395,19 @@ function getEscapeHatchCompletions( return []; } - // Calculate the range from key start to current position - // This covers "key: " so we can replace it with "key:\n - " or "key:\n " - const editRange = { + // For VS Code compatibility, we use a cursor-position range for the main textEdit + // and additionalTextEdits to clean up the key portion. This prevents VS Code from + // filtering out escape hatches based on the key text (e.g., "runs-on: "). + // + // Main textEdit: insert at cursor position (newline + indented content) + // additionalTextEdits: replace "key: " with "key:" (removes trailing space) + const cursorRange = { + start: {line: position.line, character: position.character}, + end: {line: position.line, character: position.character} + }; + + // Range from key start to cursor - used to replace "key: " with "key:" in additionalTextEdits + const keyToCursorRange = { start: {line: keyRange.start.line - 1, character: keyRange.start.column - 1}, end: {line: position.line, character: position.character} }; @@ -400,9 +417,15 @@ function getEscapeHatchCompletions( label: "(switch to list)", sortText: "zzz_switch_1", textEdit: { - range: editRange, - newText: `${keyName}:\n${indentation}- ` - } + range: cursorRange, + newText: `\n${indentation}- ` + }, + additionalTextEdits: [ + { + range: keyToCursorRange, + newText: `${keyName}:` + } + ] }); } @@ -411,9 +434,15 @@ function getEscapeHatchCompletions( label: "(switch to mapping)", sortText: "zzz_switch_2", textEdit: { - range: editRange, - newText: `${keyName}:\n${indentation}` - } + range: cursorRange, + newText: `\n${indentation}` + }, + additionalTextEdits: [ + { + range: keyToCursorRange, + newText: `${keyName}:` + } + ] }); } diff --git a/languageservice/src/value-providers/config.ts b/languageservice/src/value-providers/config.ts index 11d4872..2998dc5 100644 --- a/languageservice/src/value-providers/config.ts +++ b/languageservice/src/value-providers/config.ts @@ -27,6 +27,12 @@ export interface Value { range: {start: {line: number; character: number}; end: {line: number; character: number}}; newText: string; }; + + /** Additional text edits to apply after the main edit (e.g., cleanup edits) */ + additionalTextEdits?: { + range: {start: {line: number; character: number}; end: {line: number; character: number}}; + newText: string; + }[]; } export enum ValueProviderKind {