Skip to content
This repository has been archived by the owner on Dec 13, 2018. It is now read-only.

Add support for Rust #1606

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
Open
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
141 changes: 141 additions & 0 deletions pkg/nuclide-rust/lib/BuckIntegration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the license found in the LICENSE file in
* the root directory of this source tree.
*
* @flow
* @format
*/

import type {BusySignalService, BusySignalOptions} from 'atom-ide-ui';
import type {TaskInfo} from '../../nuclide-buck/lib/types';
import type {
AtomLanguageService,
LanguageService,
} from '../../nuclide-language-service';

import {getLogger} from 'log4js';
import fsPromise from 'nuclide-commons/fsPromise';
import {
getRustBuildFile,
getRustInputs,
getSaveAnalysisTargets,
isRustBuildRuleType,
normalizeNameForBuckQuery,
} from './BuckUtils';

import * as BuckService from '../../nuclide-buck-rpc';

const logger = getLogger('nuclide-rust');

export async function updateRlsBuildForTask(
task: TaskInfo,
service: AtomLanguageService<LanguageService>,
busySignalService: ?BusySignalService,
) {
if (!isRustBuildRuleType(task.buildRuleType.type)) {
return;
}
const buildTarget = normalizeNameForBuckQuery(task.buildTarget);

// Output is relative to Buck root but the built target may be managed by a
// Buck cell (nested Buck root).
// Here, Buck returns input paths relative to the possible cell, but the build
// file always relative to the current Buck root. Because of that, we use the
// build file path to determine the possible Buck cell root to which the
// inputs are relative to.
// FIXME: This is a bug in Buck, only query for files when the output is fixed.
const [relativeBuildFile, files] = await Promise.all([
getRustBuildFile(task.buckRoot, buildTarget),
getRustInputs(task.buckRoot, buildTarget),
]);
// Not a Rust build target, ignore
if (relativeBuildFile == null || files.length === 0) {
return;
}

const buildFile = `${task.buckRoot}/${relativeBuildFile}`;
const buckRoot = await BuckService.getRootForPath(buildFile);
if (buckRoot == null) {
logger.error(`Couldn't find Buck root for ${buildFile}`);
return;
}

logger.debug(`Detected Buck root: ${buckRoot}`);
// We need only to pick a representative file to get a related lang service
const fileUri = buckRoot + '/' + files[0];

const langService = await service.getLanguageServiceForUri(fileUri);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's an invalid assumption here that the multi-project language service will only have one LSP server that we need to update its build command (while in reality there can be multiple buck roots, each having a different LSP server -- according to the project files config below).

Do you need to getAllLanguageServices or observeLanguageServices?
Can you test with multiple buck roots to see if things work properly?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understood that MultiLspLanguageService spawn separate LSP server per appropriate 'top-level' project config files like .hhconfig and that calling this seems like a good way to retrieve a handle to appropriate LSP server that's responsible for the fileUri we're building.

However, I did assume there's going to be a single Buck root; I'll see if it works with multiple ones.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mostafaeweda sorry it took so long! Just tested this with multiple buck roots, using https://github.com/Xanewok/rust-buck-skeleton and building both //server:server and nested//:nested. This correctly retrieved the LSP server at root dir and nested/, respectively, so this looks good so far =)

if (langService == null) {
atom.notifications.addError(`[nuclide-rust] couldn't find language service
for target ${buildTarget}`);
return;
}

// Since `buck` execution is not trivial - the command may be overriden, needs
// to inherit the environment, passes internal FB USER to env etc. the RLS
// can't just invoke that.
// Instead, we build now, copy paths to resulting .json analysis artifacts to
// a temp file and just use `cat $TMPFILE` as a dummy build command.
const doSaveAnalysisBuild = () =>
getSaveAnalysisTargets(task.buckRoot, buildTarget).then(analysisTargets => {
logger.debug(`analysisTargets: ${analysisTargets.join('\n')}`);

return BuckService.build(task.buckRoot, analysisTargets);
});

const buildReport = await reportBusyWhile(
busySignalService,
'[nuclide-rust] Indexing...',
doSaveAnalysisBuild,
);

if (!buildReport.success) {
atom.notifications.addError('[nuclide-rust] save-analysis build failed');
return;
}

const artifacts: Array<string> = [];
Object.values(buildReport.results)
// TODO: https://buckbuild.com/command/build.html specifies that for
// FETCHED_FROM_CACHE we might not get an output file - can we force it
// somehow? Or we always locally produce a save-analysis .json file for
// #save-analysis flavor?
.forEach((targetReport: any) =>
artifacts.push(`${buckRoot}/${targetReport.output}`),
);

const tempfile = await fsPromise.tempfile();
await fsPromise.writeFile(tempfile, artifacts.join('\n'));

// TODO: Windows?
const buildCommand = `cat ${tempfile}`;

logger.debug(`Built SA artifacts: ${artifacts.join('\n')}`);
logger.debug(`buildCommand: ${buildCommand}`);

await langService.sendLspNotification('workspace/didChangeConfiguration', {
settings: {
rust: {
unstable_features: true, // Required for build_command
build_on_save: true,
build_command: buildCommand,
},
},
});
}

function reportBusyWhile<T>(
busySignalService: ?BusySignalService,
title: string,
f: () => Promise<T>,
options?: BusySignalOptions,
): Promise<T> {
if (busySignalService) {
return busySignalService.reportBusyWhile(title, f, options);
} else {
return f();
}
}
71 changes: 71 additions & 0 deletions pkg/nuclide-rust/lib/BuckUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the license found in the LICENSE file in
* the root directory of this source tree.
*
* @flow strict-local
* @format
*/

import * as BuckService from '../../nuclide-buck-rpc';

export type BuildTarget = string;

export function isRustBuildRuleType(type_: string): boolean {
return type_.startsWith('rust_');
}

export async function getRustBuildFile(
buckRoot: string,
buildTarget: BuildTarget,
): Promise<?string> {
return BuckService.query(
buckRoot,
`buildfile(kind('^rust_.*', ${buildTarget}))`,
[],
).then(buildfiles => buildfiles[0] || null);
}

export function getRustInputs(
buckRoot: string,
buildTarget: BuildTarget,
): Promise<Array<string>> {
return BuckService.query(
buckRoot,
`filter('.*\\.rs$', inputs(kind('^rust_.*', ${buildTarget})))`,
[],
);
}

export async function getSaveAnalysisTargets(
buckRoot: string,
buildTarget: BuildTarget,
): Promise<Array<string>> {
// Save-analysis build flavor is only supported by rust_{binary, library}
// kinds (so exclude prebuilt_rust_library kind)
const query: string = `kind('^rust_.*', deps(${buildTarget}))`;

const deps = await BuckService.query(buckRoot, query, []);
return deps.map(dep => dep + '#save-analysis');
}

// FIXME: Copied from nuclide-buck-rpc
// Buck query doesn't allow omitting // or adding # for flavors, this needs to be fixed in buck.
export function normalizeNameForBuckQuery(aliasOrTarget: string): BuildTarget {
let canonicalName = aliasOrTarget;
// Don't prepend // for aliases (aliases will not have colons or .)
if (
(canonicalName.indexOf(':') !== -1 || canonicalName.indexOf('.') !== -1) &&
canonicalName.indexOf('//') === -1
) {
canonicalName = '//' + canonicalName;
}
// Strip flavor string
const flavorIndex = canonicalName.indexOf('#');
if (flavorIndex !== -1) {
canonicalName = canonicalName.substr(0, flavorIndex);
}
return canonicalName;
}
124 changes: 124 additions & 0 deletions pkg/nuclide-rust/lib/RustLanguage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the license found in the LICENSE file in
* the root directory of this source tree.
*
* @flow
* @format
*/

import type {ServerConnection} from '../../nuclide-remote-connection';
import type {AtomLanguageServiceConfig} from '../../nuclide-language-service/lib/AtomLanguageService';
import type {LanguageService} from '../../nuclide-language-service/lib/LanguageService';

import featureConfig from 'nuclide-commons-atom/feature-config';
import {
AtomLanguageService,
getHostServices,
} from '../../nuclide-language-service';
import {NullLanguageService} from '../../nuclide-language-service-rpc';
import {getNotifierByConnection} from '../../nuclide-open-files';
import {getVSCodeLanguageServiceByConnection} from '../../nuclide-remote-connection';

export function getRlsPath(): string {
return (featureConfig.get('nuclide-rust.rlsPath'): any);
}

async function connectionToRustService(
connection: ?ServerConnection,
): Promise<LanguageService> {
const [fileNotifier, host] = await Promise.all([
getNotifierByConnection(connection),
getHostServices(),
]);
const service = getVSCodeLanguageServiceByConnection(connection);

const lspService = await service.createMultiLspLanguageService(
'rust',
getRlsPath(),
[],
{
fileNotifier,
host,
projectFileNames: ['Cargo.toml', '.buckconfig'],
fileExtensions: ['.rs'],
logCategory: 'nuclide-rust',
logLevel: 'TRACE',
useOriginalEnvironment: true,
additionalLogFilesRetentionPeriod: 5 * 60 * 1000, // 5 minutes
waitForDiagnostics: true,
initializationOptions: {
// Don't let RLS eagerly build (and fail crashing while finding a
// Cargo.toml if the project uses Buck) for now.
// TODO: Pass initial config (at least the `build_command`).
// https://github.com/rust-lang-nursery/rls/issues/1026
// Without this the RLS can still can crash when the user starts
// modifying .rs files.
omitInitBuild: true,
},
},
);

return lspService || new NullLanguageService();
}

export const atomConfig: AtomLanguageServiceConfig = {
name: 'Rust',
grammars: ['source.rust'],
diagnostics: {
version: '0.2.0',
analyticsEventName: 'rust.observe-diagnostics',
},
definition: {
version: '0.1.0',
priority: 1,
definitionEventName: 'rust.definition',
},
codeAction: {
version: '0.1.0',
priority: 1,
analyticsEventName: 'rust.codeAction',
applyAnalyticsEventName: 'rust.applyCodeAction',
},
codeFormat: {
version: '0.1.0',
priority: 1,
analyticsEventName: 'rust.codeFormat',
canFormatRanges: true,
canFormatAtPosition: true,
},
findReferences: {
version: '0.1.0',
analyticsEventName: 'rust.findReferences',
},
rename: {
version: '0.0.0',
priority: 1,
analyticsEventName: 'rust.rename',
},
autocomplete: {
inclusionPriority: 1,
suggestionPriority: 3,
excludeLowerPriority: false,
analytics: {
eventName: 'nuclide-rust',
shouldLogInsertedSuggestion: false,
},
disableForSelector: '.source.rust .comment, .source.rust .string',
autocompleteCacherConfig: null,
supportsResolve: false,
},
typeHint: {
version: '0.0.0',
priority: 1,
analyticsEventName: 'rust.typeHint',
},
};

export function createRustLanguageService(): AtomLanguageService<
LanguageService,
> {
return new AtomLanguageService(connectionToRustService, atomConfig);
}
Loading