Skip to content

feat: provide snippets for attribute #1509

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions client/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,12 @@ function constructArgs(ctx: vscode.ExtensionContext): string[] {
args.push('--includeAutomaticOptionalChainCompletions');
}

const includeCompletionsWithSnippetText =
config.get<boolean>('angular.suggest.includeCompletionsWithSnippetText');
if (includeCompletionsWithSnippetText) {
args.push('--includeCompletionsWithSnippetText');
}

const tsdk: string|null = config.get('typescript.tsdk', null);
const tsProbeLocations = getProbeLocations(tsdk, ctx.extensionPath);
args.push('--tsProbeLocations', tsProbeLocations.join(','));
Expand Down
54 changes: 54 additions & 0 deletions integration/lsp/ivy_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,60 @@ describe('auto-apply optional chaining', () => {
});
});

describe('insert snippet text', () => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; /* 10 seconds */

let client: MessageConnection;
beforeEach(async () => {
client = createConnection({
ivy: true,
includeCompletionsWithSnippetText: true,
});
// If debugging, set to
// - lsp.Trace.Messages to inspect request/response/notification, or
// - lsp.Trace.Verbose to inspect payload
client.trace(lsp.Trace.Off, createTracer());
client.listen();
await initializeServer(client);
});

afterEach(() => {
client.dispose();
});

it('should be able to complete for an attribute with the value is empty', async () => {
openTextDocument(client, FOO_TEMPLATE, `<my-app appOut></my-app>`);
const languageServiceEnabled = await waitForNgcc(client);
expect(languageServiceEnabled).toBeTrue();
const response = await client.sendRequest(lsp.CompletionRequest.type, {
textDocument: {
uri: FOO_TEMPLATE_URI,
},
position: {line: 0, character: 14},
}) as lsp.CompletionItem[];
const completion = response.find(i => i.label === '(appOutput)')!;
expect(completion.kind).toEqual(lsp.CompletionItemKind.Property);
expect(completion.insertTextFormat).toEqual(lsp.InsertTextFormat.Snippet);
expect((completion.textEdit as lsp.TextEdit).newText).toEqual('(appOutput)="$1"');
});

it('should not be included in the completion for an attribute with a value', async () => {
openTextDocument(client, FOO_TEMPLATE, `<my-app [appInput]="1"></my-app>`);
const languageServiceEnabled = await waitForNgcc(client);
expect(languageServiceEnabled).toBeTrue();
const response = await client.sendRequest(lsp.CompletionRequest.type, {
textDocument: {
uri: FOO_TEMPLATE_URI,
},
position: {line: 0, character: 17},
}) as lsp.CompletionItem[];
const completion = response.find(i => i.label === 'appInput')!;
expect(completion.kind).toEqual(lsp.CompletionItemKind.Property);
expect(completion.insertTextFormat).toBeUndefined;
expect((completion.textEdit as lsp.TextEdit).newText).toEqual('appInput');
});
});

function onNgccProgress(client: MessageConnection): Promise<string> {
return new Promise(resolve => {
client.onProgress(NgccProgressType, NgccProgressToken, (params: NgccProgress) => {
Expand Down
15 changes: 14 additions & 1 deletion integration/lsp/test_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {PROJECT_PATH, SERVER_PATH} from '../test_constants';
export interface ServerOptions {
ivy: boolean;
includeAutomaticOptionalChainCompletions?: boolean;
includeCompletionsWithSnippetText?: boolean;
}

export function createConnection(serverOptions: ServerOptions): MessageConnection {
Expand All @@ -33,6 +34,9 @@ export function createConnection(serverOptions: ServerOptions): MessageConnectio
if (serverOptions.includeAutomaticOptionalChainCompletions) {
argv.push('--includeAutomaticOptionalChainCompletions');
}
if (serverOptions.includeCompletionsWithSnippetText) {
argv.push('--includeCompletionsWithSnippetText');
}
const server = fork(SERVER_PATH, argv, {
cwd: PROJECT_PATH,
// uncomment to debug server process
Expand Down Expand Up @@ -61,7 +65,16 @@ export function initializeServer(client: MessageConnection): Promise<lsp.Initial
*/
processId: process.pid,
rootUri: `file://${PROJECT_PATH}`,
capabilities: {},
capabilities: {
textDocument: {
completion: {
completionItem: {
snippetSupport: true,
}
},
moniker: {},
}
},
/**
* Options are 'off' | 'messages' | 'verbose'.
* To debug test failure, set to 'verbose'.
Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@
"type": "boolean",
"default": true,
"description": "Enable/disable showing completions on potentially undefined values that insert an optional chain call. Requires TS 3.7+ and strict null checks to be enabled."
},
"angular.suggest.includeCompletionsWithSnippetText": {
"type": "boolean",
"default": true,
"markdownDescription": "Enable/disable snippet completions from Angular language server. Requires using TypeScript 4.3+ in the workspace and the `legacy View Engine` option to be disabled."
}
}
},
Expand Down
2 changes: 2 additions & 0 deletions server/src/cmdline_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ interface CommandLineOptions {
ngProbeLocations: string[];
tsProbeLocations: string[];
includeAutomaticOptionalChainCompletions: boolean;
includeCompletionsWithSnippetText: boolean;
}

export function parseCommandLine(argv: string[]): CommandLineOptions {
Expand All @@ -51,6 +52,7 @@ export function parseCommandLine(argv: string[]): CommandLineOptions {
tsProbeLocations: parseStringArray(argv, '--tsProbeLocations'),
includeAutomaticOptionalChainCompletions:
hasArgument(argv, '--includeAutomaticOptionalChainCompletions'),
includeCompletionsWithSnippetText: hasArgument(argv, '--includeCompletionsWithSnippetText'),
};
}

Expand Down
3 changes: 3 additions & 0 deletions server/src/completion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ export function tsCompletionEntryToLspCompletionItem(
// range will include the dot. the `insertText` should be assigned to the `filterText` to filter
// the completion items.
item.filterText = entry.insertText;
if (entry.isSnippet) {
item.insertTextFormat = lsp.InsertTextFormat.Snippet;
}
}

item.data = {
Expand Down
3 changes: 2 additions & 1 deletion server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ function main() {
resolvedNgLsPath: ng.resolvedPath,
ivy: isG3 ? true : options.ivy,
logToConsole: options.logToConsole,
includeAutomaticOptionalChainCompletions: options.includeAutomaticOptionalChainCompletions
includeAutomaticOptionalChainCompletions: options.includeAutomaticOptionalChainCompletions,
includeCompletionsWithSnippetText: options.includeCompletionsWithSnippetText,
});

// Log initialization info
Expand Down
14 changes: 12 additions & 2 deletions server/src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export interface SessionOptions {
ivy: boolean;
logToConsole: boolean;
includeAutomaticOptionalChainCompletions: boolean;
includeCompletionsWithSnippetText: boolean;
}

enum LanguageId {
Expand All @@ -54,6 +55,8 @@ export class Session {
private readonly logToConsole: boolean;
private readonly openFiles = new MruTracker();
private readonly includeAutomaticOptionalChainCompletions: boolean;
private readonly includeCompletionsWithSnippetText: boolean;
private snippetSupport: boolean|undefined;
// Tracks the spawn order and status of the `ngcc` processes. This allows us to ensure we enable
// the LS in the same order the projects were created in.
private projectNgccQueue: Array<{project: ts.server.Project, done: boolean}> = [];
Expand All @@ -71,6 +74,7 @@ export class Session {
constructor(options: SessionOptions) {
this.includeAutomaticOptionalChainCompletions =
options.includeAutomaticOptionalChainCompletions;
this.includeCompletionsWithSnippetText = options.includeCompletionsWithSnippetText;
this.logger = options.logger;
this.ivy = options.ivy;
this.logToConsole = options.logToConsole;
Expand Down Expand Up @@ -605,6 +609,8 @@ export class Session {
}

private onInitialize(params: lsp.InitializeParams): lsp.InitializeResult {
this.snippetSupport =
params.capabilities.textDocument?.completion?.completionItem?.snippetSupport;
const serverOptions: ServerOptions = {
logFile: this.logger.getLogFileName(),
};
Expand Down Expand Up @@ -1007,10 +1013,14 @@ export class Session {
const offset = lspPositionToTsPosition(scriptInfo, params.position);

let options: ts.GetCompletionsAtPositionOptions = {};
if (this.includeAutomaticOptionalChainCompletions) {
const includeCompletionsWithSnippetText =
this.includeCompletionsWithSnippetText && this.snippetSupport;
if (this.includeAutomaticOptionalChainCompletions || includeCompletionsWithSnippetText) {
options = {
includeAutomaticOptionalChainCompletions: this.includeAutomaticOptionalChainCompletions,
includeCompletionsWithInsertText: this.includeAutomaticOptionalChainCompletions,
includeCompletionsWithSnippetText: includeCompletionsWithSnippetText,
includeCompletionsWithInsertText:
this.includeAutomaticOptionalChainCompletions || includeCompletionsWithSnippetText,
};
}

Expand Down