-
Notifications
You must be signed in to change notification settings - Fork 681
Add support for Rust #1606
base: master
Are you sure you want to change the base?
Add support for Rust #1606
Changes from 16 commits
b4dc64b
2cd3106
0809729
a446661
6722738
b2f7f11
15d4b3c
9d6297d
4435578
798fed2
ab2bc89
ca76f6c
9ce74be
9f64faf
8797da0
384c616
2779b30
7b9c326
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,137 @@ | ||
| /** | ||
| * 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 { | ||
| getRustInputs, | ||
| getSaveAnalysisTargets, | ||
| normalizeNameForBuckQuery, | ||
| getRustBuildFile, | ||
| } 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, | ||
| ) { | ||
| 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); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| 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(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| /** | ||
| * 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 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; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| /** | ||
| * 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', | ||
| }, | ||
| codeFormat: { | ||
| version: '0.1.0', | ||
| priority: 1, | ||
| analyticsEventName: 'rust.formatCode', | ||
| canFormatRanges: true, | ||
| canFormatAtPosition: true, | ||
| }, | ||
| findReferences: { | ||
| version: '0.1.0', | ||
| analyticsEventName: 'rust.get-references', | ||
| }, | ||
| 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); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<LanguageService>; | ||
| _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); |
Uh oh!
There was an error while loading. Please reload this page.