diff --git a/pkg/nuclide-rust/lib/BuckIntegration.js b/pkg/nuclide-rust/lib/BuckIntegration.js new file mode 100644 index 0000000000..ff15611790 --- /dev/null +++ b/pkg/nuclide-rust/lib/BuckIntegration.js @@ -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, + 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); + 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 = []; + 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( + busySignalService: ?BusySignalService, + title: string, + f: () => Promise, + options?: BusySignalOptions, +): Promise { + if (busySignalService) { + return busySignalService.reportBusyWhile(title, f, options); + } else { + return f(); + } +} diff --git a/pkg/nuclide-rust/lib/BuckUtils.js b/pkg/nuclide-rust/lib/BuckUtils.js new file mode 100644 index 0000000000..39e51915e9 --- /dev/null +++ b/pkg/nuclide-rust/lib/BuckUtils.js @@ -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 { + return BuckService.query( + buckRoot, + `buildfile(kind('^rust_.*', ${buildTarget}))`, + [], + ).then(buildfiles => buildfiles[0] || null); +} + +export function getRustInputs( + buckRoot: string, + buildTarget: BuildTarget, +): Promise> { + return BuckService.query( + buckRoot, + `filter('.*\\.rs$', inputs(kind('^rust_.*', ${buildTarget})))`, + [], + ); +} + +export async function getSaveAnalysisTargets( + buckRoot: string, + buildTarget: BuildTarget, +): Promise> { + // 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; +} diff --git a/pkg/nuclide-rust/lib/RustLanguage.js b/pkg/nuclide-rust/lib/RustLanguage.js new file mode 100644 index 0000000000..670f1c2b57 --- /dev/null +++ b/pkg/nuclide-rust/lib/RustLanguage.js @@ -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 { + 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); +} diff --git a/pkg/nuclide-rust/lib/main.js b/pkg/nuclide-rust/lib/main.js new file mode 100644 index 0000000000..4719e2cdd1 --- /dev/null +++ b/pkg/nuclide-rust/lib/main.js @@ -0,0 +1,65 @@ +/** + * 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 {BuckTaskRunnerService} from '../../nuclide-buck/lib/types'; +import type {BusySignalService} from 'atom-ide-ui'; +import type { + AtomLanguageService, + LanguageService, +} from '../../nuclide-language-service'; + +import createPackage from 'nuclide-commons-atom/createPackage'; +import UniversalDisposable from 'nuclide-commons/UniversalDisposable'; +import {createRustLanguageService} from './RustLanguage'; + +import {updateRlsBuildForTask} from './BuckIntegration'; + +const DISCLAIMER = `[nuclide-rust] Support for Buck-managed Rust +projects is currently experimental. For it to work correctly, please build +the target you plan on working using Buck toolbar.`; + +class Activation { + _rustLanguageService: AtomLanguageService; + _subscriptions: UniversalDisposable; + _busySignalService: ?BusySignalService; + + constructor(rawState: ?Object) { + atom.notifications.addInfo(DISCLAIMER); + + this._rustLanguageService = createRustLanguageService(); + this._rustLanguageService.activate(); + + this._subscriptions = new UniversalDisposable(this._rustLanguageService); + } + + consumeBuckTaskRunner(service: BuckTaskRunnerService): IDisposable { + return service.onDidCompleteTask(task => + updateRlsBuildForTask( + task, + this._rustLanguageService, + this._busySignalService, + ), + ); + } + + consumeBusySignal(busySignalService: BusySignalService): IDisposable { + this._busySignalService = busySignalService; + return new UniversalDisposable(() => { + this._busySignalService = null; + }); + } + + dispose(): void { + this._subscriptions.dispose(); + } +} + +createPackage(module.exports, Activation); diff --git a/pkg/nuclide-rust/package.json b/pkg/nuclide-rust/package.json new file mode 100644 index 0000000000..cafd505452 --- /dev/null +++ b/pkg/nuclide-rust/package.json @@ -0,0 +1,40 @@ +{ + "name": "nuclide-rust", + "main": "./lib/main.js", + "version": "0.0.0", + "description": "Provides Rust language support in Nuclide", + "author": "NEEDS OWNER", + "atomTestRunner": "../../lib/test-runner-entry.js", + "nuclide": { + "packageType": "AtomPackage", + "configMetadata": { + "pathComponents": [ + "Language", + "Rust" + ] + }, + "config": { + "rlsPath": { + "title": "Rust Language Server path", + "type": "string", + "default": "rls", + "description": "Path to the RLS executable. By default uses rustup-managed 'rls' executable." + } + } + }, + "activationHooks": [ + "language-rust:grammar-used" + ], + "consumedServices": { + "atom-ide-busy-signal": { + "versions": { + "0.1.0": "consumeBusySignal" + } + }, + "nuclide.buck-task-runner": { + "versions": { + "0.0.0": "consumeBuckTaskRunner" + } + } + } +}