diff --git a/src/compiler/debug.ts b/src/compiler/debug.ts index 2e6b87a30e6..705ab949899 100644 --- a/src/compiler/debug.ts +++ b/src/compiler/debug.ts @@ -351,6 +351,10 @@ namespace ts { return formatEnum(kind, (ts as any).SyntaxKind, /*isFlags*/ false); } + export function formatSnippetKind(kind: SnippetKind | undefined): string { + return formatEnum(kind, (ts as any).SnippetKind, /*isFlags*/ false); + } + export function formatNodeFlags(flags: NodeFlags | undefined): string { return formatEnum(flags, (ts as any).NodeFlags, /*isFlags*/ true); } diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index 5667e792b46..5c9aaf23c11 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -1302,7 +1302,13 @@ namespace ts { currentParenthesizerRule = undefined; } - function pipelineEmitWithHintWorker(hint: EmitHint, node: Node): void { + function pipelineEmitWithHintWorker(hint: EmitHint, node: Node, allowSnippets = true): void { + if (allowSnippets) { + const snippet = getSnippetElement(node); + if (snippet) { + return emitSnippetNode(hint, node, snippet); + } + } if (hint === EmitHint.SourceFile) return emitSourceFile(cast(node, isSourceFile)); if (hint === EmitHint.IdentifierName) return emitIdentifier(cast(node, isIdentifier)); if (hint === EmitHint.JsxAttributeValue) return emitLiteral(cast(node, isStringLiteral), /*jsxAttributeEscape*/ true); @@ -1937,6 +1943,30 @@ namespace ts { } } + // + // Snippet Elements + // + + function emitSnippetNode(hint: EmitHint, node: Node, snippet: SnippetElement) { + switch (snippet.kind) { + case SnippetKind.Placeholder: + return emitPlaceholder(hint, node, snippet); + case SnippetKind.TabStop: + return emitTabStop(snippet); + } + } + + function emitPlaceholder(hint: EmitHint, node: Node, snippet: Placeholder) { + // write(`\$\{${order}:${defaultText}\}`); + write(`\$\{${snippet.order}:`); + pipelineEmitWithHintWorker(hint, node, /*allowSnippets*/ false); + write(`\}`); + } + + function emitTabStop(snippet: TabStop) { + write(`\$${snippet.order}`); + } + // // Identifiers // diff --git a/src/compiler/factory/emitNode.ts b/src/compiler/factory/emitNode.ts index 8f032fd8b76..fa4489ee7da 100644 --- a/src/compiler/factory/emitNode.ts +++ b/src/compiler/factory/emitNode.ts @@ -256,6 +256,22 @@ namespace ts { } } + /** + * Gets the SnippetElement of a node. + */ + export function getSnippetElement(node: Node): SnippetElement | undefined { + return node.emitNode?.snippetElement; + } + + /** + * Sets the SnippetElement of a node. + */ + export function setSnippetElement(node: T, snippet: SnippetElement): T { + const emitNode = getOrCreateEmitNode(node); + emitNode.snippetElement = snippet; + return node; + } + /* @internal */ export function ignoreSourceNewlines(node: T): T { getOrCreateEmitNode(node).flags |= EmitFlags.IgnoreSourceNewlines; diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 0888e880729..55e452fb30c 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -6740,16 +6740,14 @@ namespace ts { snippetElement?: SnippetElement; // Snippet element of the node } - export interface SnippetElement { - kind: SnippetKind; - } + export type SnippetElement = TabStop | Placeholder; - export interface TabStop extends SnippetElement { + export interface TabStop { kind: SnippetKind.TabStop; order: number; } - export interface PlaceHolder extends SnippetElement { + export interface Placeholder { kind: SnippetKind.Placeholder; order: number; } diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 19784af87c0..a96753dcf08 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -3142,7 +3142,7 @@ namespace ts { return undefined; } - export function isKeyword(token: SyntaxKind): boolean { + export function isKeyword(token: SyntaxKind): token is KeywordSyntaxKind { return SyntaxKind.FirstKeyword <= token && token <= SyntaxKind.LastKeyword; } @@ -3327,7 +3327,7 @@ namespace ts { return node.escapedText === "push" || node.escapedText === "unshift"; } - export function isParameterDeclaration(node: VariableLikeDeclaration) { + export function isParameterDeclaration(node: VariableLikeDeclaration): node is ParameterDeclaration { const root = getRootDeclaration(node); return root.kind === SyntaxKind.Parameter; } diff --git a/src/services/completions.ts b/src/services/completions.ts index c1ba3f4045a..3f0447347d2 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -760,8 +760,11 @@ namespace ts.Completions { let body; if (preferences.includeCompletionsWithSnippetText) { isSnippet = true; - const tabStopStatement = factory.createExpressionStatement(factory.createIdentifier("$1")); - body = factory.createBlock([tabStopStatement], /* multiline */ true); + // const tabStopStatement = factory.createExpressionStatement(factory.createIdentifier("$1")); + // body = factory.createBlock([tabStopStatement], /* multiline */ true); + const emptyStatement = factory.createExpressionStatement(factory.createIdentifier("")); + setSnippetElement(emptyStatement, { kind: SnippetKind.TabStop, order: 0 }); + body = factory.createBlock([emptyStatement], /* multiline */ true); } else { body = factory.createBlock([], /* multiline */ true); @@ -813,6 +816,7 @@ namespace ts.Completions { concatenate([factory.createModifier(SyntaxKind.OverrideKeyword)], node.modifiers), ); } + addSnippets(node); completionNodes.push(node); }, body); @@ -823,6 +827,32 @@ namespace ts.Completions { return { insertText, isSnippet }; } + + function addSnippets(node: Node): void { + let order = 1; + addSnippetsWorker(node); + + function addSnippetsWorker(node: Node) { + if (isVariableLike(node) && isParameterDeclaration(node)) { + // Placeholder + setSnippetElement(node.name, { kind: SnippetKind.Placeholder, order }); + order += 1; + if (node.type) { + setSnippetElement(node.type, { kind: SnippetKind.Placeholder, order }); + order += 1; + } + } + else if (isFunctionLikeDeclaration(node)) { + if (node.type) { + setSnippetElement(node.type, { kind: SnippetKind.Placeholder, order }); + order += 1; + } + } + + forEachChild(node, addSnippetsWorker); + } + } + function originToCompletionEntryData(origin: SymbolOriginInfoExport): CompletionEntryData | undefined { return { exportName: origin.exportName, diff --git a/src/services/snippets.ts b/src/services/snippets.ts new file mode 100644 index 00000000000..b1590d57c27 --- /dev/null +++ b/src/services/snippets.ts @@ -0,0 +1,23 @@ +/* @internal */ +namespace ts.snippets { + // interface SnippetWriter extends EmitTextWriter, PrintHandlers {} + + // function createSnippetWriter(newLine: string): SnippetWriter { + // const substituteNode = (hint: EmitHint, node: Node) => { + // switch (hint) { + // case EmitHint.IdentifierName: + // // >> TODO: do escaping here + // return node; + // } + // return node; + // }; + + // const writer = createTextWriter(newLine); + + + // return { + // ...writer, + // substituteNode, + // }; + // } +} diff --git a/src/services/tsconfig.json b/src/services/tsconfig.json index 1e0934804ed..729386a3b77 100644 --- a/src/services/tsconfig.json +++ b/src/services/tsconfig.json @@ -33,6 +33,7 @@ "preProcess.ts", "rename.ts", "smartSelection.ts", + "snippets.ts", "signatureHelp.ts", "inlayHints.ts", "sourcemaps.ts",