1import * as vscode from 'vscode';
2import * as vscodelc from 'vscode-languageclient';
3import * as semanticHighlighting from './semantic-highlighting';
4
5/**
6 * Method to get workspace configuration option
7 * @param option name of the option (e.g. for clangd.path should be path)
8 * @param defaultValue default value to return if option is not set
9 */
10function getConfig<T>(option: string, defaultValue?: any): T {
11  const config = vscode.workspace.getConfiguration('clangd');
12  return config.get<T>(option, defaultValue);
13}
14
15namespace SwitchSourceHeaderRequest {
16export const type =
17    new vscodelc.RequestType<vscodelc.TextDocumentIdentifier, string|undefined,
18                             void, void>('textDocument/switchSourceHeader');
19}
20
21class FileStatus {
22  private statuses = new Map<string, any>();
23  private readonly statusBarItem =
24      vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 10);
25
26  onFileUpdated(fileStatus: any) {
27    const filePath = vscode.Uri.parse(fileStatus.uri);
28    this.statuses.set(filePath.fsPath, fileStatus);
29    this.updateStatus();
30  }
31
32  updateStatus() {
33    const path = vscode.window.activeTextEditor.document.fileName;
34    const status = this.statuses.get(path);
35    if (!status) {
36      this.statusBarItem.hide();
37      return;
38    }
39    this.statusBarItem.text = `clangd: ` + status.state;
40    this.statusBarItem.show();
41  }
42
43  clear() {
44    this.statuses.clear();
45    this.statusBarItem.hide();
46  }
47
48  dispose() { this.statusBarItem.dispose(); }
49}
50
51class ClangdLanguageClient extends vscodelc.LanguageClient {
52  // Override the default implementation for failed requests. The default
53  // behavior is just to log failures in the output panel, however output panel
54  // is designed for extension debugging purpose, normal users will not open it,
55  // thus when the failure occurs, normal users doesn't know that.
56  //
57  // For user-interactive operations (e.g. applyFixIt, applyTweaks), we will
58  // prompt up the failure to users.
59  logFailedRequest(rpcReply: vscodelc.RPCMessageType, error: any) {
60    if (error instanceof vscodelc.ResponseError &&
61        rpcReply.method === "workspace/executeCommand")
62      vscode.window.showErrorMessage(error.message);
63    // Call default implementation.
64    super.logFailedRequest(rpcReply, error);
65  }
66}
67
68/**
69 *  this method is called when your extension is activate
70 *  your extension is activated the very first time the command is executed
71 */
72export function activate(context: vscode.ExtensionContext) {
73  const syncFileEvents = getConfig<boolean>('syncFileEvents', true);
74
75  const clangd: vscodelc.Executable = {
76    command : getConfig<string>('path'),
77    args : getConfig<string[]>('arguments')
78  };
79  const traceFile = getConfig<string>('trace');
80  if (!!traceFile) {
81    const trace = {CLANGD_TRACE : traceFile};
82    clangd.options = {env : {...process.env, ...trace}};
83  }
84  const serverOptions: vscodelc.ServerOptions = clangd;
85
86  const clientOptions: vscodelc.LanguageClientOptions = {
87        // Register the server for c-family and cuda files.
88        documentSelector: [
89            { scheme: 'file', language: 'c' },
90            { scheme: 'file', language: 'cpp' },
91            // cuda is not supported by vscode, but our extension does.
92            { scheme: 'file', language: 'cuda' },
93            { scheme: 'file', language: 'objective-c'},
94            { scheme: 'file', language: 'objective-cpp'}
95        ],
96        synchronize: !syncFileEvents ? undefined : {
97        // FIXME: send sync file events when clangd provides implementations.
98        },
99        initializationOptions: { clangdFileStatus: true },
100        // Do not switch to output window when clangd returns output
101        revealOutputChannelOn: vscodelc.RevealOutputChannelOn.Never
102    };
103
104  const clangdClient = new ClangdLanguageClient('Clang Language Server',
105                                                serverOptions, clientOptions);
106  if (getConfig<boolean>('semanticHighlighting')) {
107    const semanticHighlightingFeature =
108        new semanticHighlighting.SemanticHighlightingFeature(clangdClient,
109                                                             context);
110    context.subscriptions.push(
111        vscode.Disposable.from(semanticHighlightingFeature));
112    clangdClient.registerFeature(semanticHighlightingFeature);
113  }
114  console.log('Clang Language Server is now active!');
115  context.subscriptions.push(clangdClient.start());
116  context.subscriptions.push(vscode.commands.registerCommand(
117      'clangd-vscode.switchheadersource', async () => {
118        const uri =
119            vscode.Uri.file(vscode.window.activeTextEditor.document.fileName);
120        if (!uri) {
121          return;
122        }
123        const docIdentifier =
124            vscodelc.TextDocumentIdentifier.create(uri.toString());
125        const sourceUri = await clangdClient.sendRequest(
126            SwitchSourceHeaderRequest.type, docIdentifier);
127        if (!sourceUri) {
128          return;
129        }
130        const doc = await vscode.workspace.openTextDocument(
131            vscode.Uri.parse(sourceUri));
132        vscode.window.showTextDocument(doc);
133      }));
134  const status = new FileStatus();
135  context.subscriptions.push(vscode.Disposable.from(status));
136  context.subscriptions.push(vscode.window.onDidChangeActiveTextEditor(
137      () => { status.updateStatus(); }));
138  context.subscriptions.push(clangdClient.onDidChangeState(({newState}) => {
139    if (newState == vscodelc.State.Running) {
140      // clangd starts or restarts after crash.
141      clangdClient.onNotification(
142          'textDocument/clangd.fileStatus',
143          (fileStatus) => { status.onFileUpdated(fileStatus); });
144    } else if (newState == vscodelc.State.Stopped) {
145      // Clear all cached statuses when clangd crashes.
146      status.clear();
147    }
148  }));
149  // An empty place holder for the activate command, otherwise we'll get an
150  // "command is not registered" error.
151  context.subscriptions.push(vscode.commands.registerCommand(
152      'clangd-vscode.activate', async () => {}));
153}
154