Use additionalTextEdits for escape hatch completions (#282)
Escape hatch completions now use a two-part edit strategy for VS Code compatibility: - Main textEdit: Inserts newline and indented content at cursor position (empty range so VS Code won't filter based on key text) - additionalTextEdits: Replaces 'key: ' with 'key:' to remove trailing space This prevents VS Code from filtering out escape hatches while still producing the correct final YAML structure.
This commit is contained in:
@@ -723,16 +723,28 @@ jobs:
|
|||||||
expect(switchToList!.sortText).toEqual("zzz_switch_1");
|
expect(switchToList!.sortText).toEqual("zzz_switch_1");
|
||||||
expect(switchToFull!.sortText).toEqual("zzz_switch_2");
|
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 listEdit = switchToList!.textEdit as TextEdit;
|
||||||
const fullEdit = switchToFull!.textEdit as TextEdit;
|
const fullEdit = switchToFull!.textEdit as TextEdit;
|
||||||
|
|
||||||
expect(listEdit.newText).toEqual("runs-on:\n - ");
|
// Main textEdit inserts newline and indented content at cursor position
|
||||||
expect(fullEdit.newText).toEqual("runs-on:\n ");
|
expect(listEdit.newText).toEqual("\n - ");
|
||||||
|
expect(fullEdit.newText).toEqual("\n ");
|
||||||
|
|
||||||
// TextEdit range should cover from key start to cursor position
|
// TextEdit range should be at cursor position (empty range)
|
||||||
expect(listEdit.range.start).toEqual({line: 3, character: 4});
|
expect(listEdit.range.start).toEqual({line: 3, character: 13});
|
||||||
expect(fullEdit.range.start).toEqual({line: 3, character: 4});
|
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 () => {
|
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 switchToList = result.find(x => x.label === "(switch to list)");
|
||||||
const textEdit = switchToList!.textEdit as TextEdit;
|
const textEdit = switchToList!.textEdit as TextEdit;
|
||||||
|
const additionalEdits = switchToList!.additionalTextEdits!;
|
||||||
|
|
||||||
// Applying this edit to "runs-on: " should produce "runs-on:\n - "
|
// Main textEdit inserts newline content at cursor
|
||||||
expect(textEdit.newText).toEqual("runs-on:\n - ");
|
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 - "
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -192,6 +192,12 @@ export async function complete(
|
|||||||
textEdit = TextEdit.insert(position, newText);
|
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 = {
|
const item: CompletionItem = {
|
||||||
label: value.label,
|
label: value.label,
|
||||||
labelDetails: value.labelDetail ? {description: value.labelDetail} : undefined,
|
labelDetails: value.labelDetail ? {description: value.labelDetail} : undefined,
|
||||||
@@ -202,7 +208,8 @@ export async function complete(
|
|||||||
value: value.description
|
value: value.description
|
||||||
},
|
},
|
||||||
tags: value.deprecated ? [CompletionItemTag.Deprecated] : undefined,
|
tags: value.deprecated ? [CompletionItemTag.Deprecated] : undefined,
|
||||||
textEdit
|
textEdit,
|
||||||
|
additionalTextEdits
|
||||||
};
|
};
|
||||||
|
|
||||||
return item;
|
return item;
|
||||||
@@ -388,9 +395,19 @@ function getEscapeHatchCompletions(
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate the range from key start to current position
|
// For VS Code compatibility, we use a cursor-position range for the main textEdit
|
||||||
// This covers "key: " so we can replace it with "key:\n - " or "key:\n "
|
// and additionalTextEdits to clean up the key portion. This prevents VS Code from
|
||||||
const editRange = {
|
// 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},
|
start: {line: keyRange.start.line - 1, character: keyRange.start.column - 1},
|
||||||
end: {line: position.line, character: position.character}
|
end: {line: position.line, character: position.character}
|
||||||
};
|
};
|
||||||
@@ -400,9 +417,15 @@ function getEscapeHatchCompletions(
|
|||||||
label: "(switch to list)",
|
label: "(switch to list)",
|
||||||
sortText: "zzz_switch_1",
|
sortText: "zzz_switch_1",
|
||||||
textEdit: {
|
textEdit: {
|
||||||
range: editRange,
|
range: cursorRange,
|
||||||
newText: `${keyName}:\n${indentation}- `
|
newText: `\n${indentation}- `
|
||||||
}
|
},
|
||||||
|
additionalTextEdits: [
|
||||||
|
{
|
||||||
|
range: keyToCursorRange,
|
||||||
|
newText: `${keyName}:`
|
||||||
|
}
|
||||||
|
]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,9 +434,15 @@ function getEscapeHatchCompletions(
|
|||||||
label: "(switch to mapping)",
|
label: "(switch to mapping)",
|
||||||
sortText: "zzz_switch_2",
|
sortText: "zzz_switch_2",
|
||||||
textEdit: {
|
textEdit: {
|
||||||
range: editRange,
|
range: cursorRange,
|
||||||
newText: `${keyName}:\n${indentation}`
|
newText: `\n${indentation}`
|
||||||
}
|
},
|
||||||
|
additionalTextEdits: [
|
||||||
|
{
|
||||||
|
range: keyToCursorRange,
|
||||||
|
newText: `${keyName}:`
|
||||||
|
}
|
||||||
|
]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,12 @@ export interface Value {
|
|||||||
range: {start: {line: number; character: number}; end: {line: number; character: number}};
|
range: {start: {line: number; character: number}; end: {line: number; character: number}};
|
||||||
newText: string;
|
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 {
|
export enum ValueProviderKind {
|
||||||
|
|||||||
Reference in New Issue
Block a user