Skip to content

Commit

Permalink
Add nuclide-swift
Browse files Browse the repository at this point in the history
Add a nuclide-swift package. nuclide-swift registers a toolbar that invokes
Swift package manager commands such as `swift build` and `swift test`.
This package makes use of SwiftPM to also provide precise compilation flags to
SourceKitten, which allows Nuclide to provide detailed type information.
  • Loading branch information
modocache committed Jul 19, 2016
1 parent 3bdb28e commit b68e0d8
Show file tree
Hide file tree
Showing 16 changed files with 642 additions and 4 deletions.
5 changes: 5 additions & 0 deletions pkg/nuclide-swift/keymaps/swift.json
@@ -0,0 +1,5 @@
{
"atom-workspace": {
"ctrl-cmd-p": "nuclide-swift:create-new-package"
}
}
47 changes: 47 additions & 0 deletions pkg/nuclide-swift/lib/CodeFormatHelpers.js
@@ -0,0 +1,47 @@
'use babel';
/* @flow */

/*
* 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.
*/

import {asyncExecute} from '../../commons-node/process';
import {trackTiming} from '../../nuclide-analytics';
import {getLogger} from '../../nuclide-logging';
import fsPromise from '../../commons-node/fsPromise';


export default class CodeFormatHelpers {
@trackTiming('nuclide-swift.formatCode')
static async formatEntireFile(editor: atom$TextEditor, range: atom$Range): Promise<{
newCursor?: number;
formatted: string;
}> {
const tempFile = await fsPromise.tempfile();
try {
const beforeFile = editor.getBuffer().getText();
await fsPromise.writeFile(tempFile, beforeFile);

const result = await asyncExecute(
'sourcekitten',
['format', '--file', tempFile],
{}
);
const afterFile = await fsPromise.readFile(tempFile, 'utf8');

return {
newCursor: null,
formatted: afterFile,
};
} catch (e) {
getLogger().error('Could not run `sourcekitten format`', e);
throw new Error('Could not run `sourcekitten format`.<br>Ensure it is installed and in your $PATH.');
} finally {
await fsPromise.unlink(tempFile);
}
}
}
56 changes: 56 additions & 0 deletions pkg/nuclide-swift/lib/main.js
Expand Up @@ -9,15 +9,21 @@
* the root directory of this source tree.
*/

import type {CodeFormatProvider} from '../../nuclide-code-format/lib/types';
import type {OutputService} from '../../nuclide-console/lib/types';
import type {CwdApi} from '../../nuclide-current-working-directory/lib/CwdApi';
import type {TaskRunnerServiceApi} from '../../nuclide-task-runner/lib/types';
import type {NuclideSideBarService} from '../../nuclide-side-bar';
import type {OutlineProvider} from '../../nuclide-outline-view';
import type {TypeHint, TypeHintProvider} from '../../nuclide-type-hint/lib/types';
import type {SwiftPMTaskRunner as SwiftPMTaskRunnerType} from './taskrunner/SwiftPMTaskRunner';
import type {SwiftPMTaskRunnerStoreState} from './taskrunner/SwiftPMTaskRunnerStoreState';

import invariant from 'assert';
import {CompositeDisposable, Disposable} from 'atom';
import {SwiftPMTaskRunner} from './taskrunner/SwiftPMTaskRunner';
import {getOutline} from './taskrunner/SwiftOutlineProvider';
import CodeFormatHelpers from './CodeFormatHelpers';

let _disposables: ?CompositeDisposable = null;
let _taskRunner: ?SwiftPMTaskRunnerType = null;
Expand All @@ -29,6 +35,20 @@ export function activate(rawState: ?Object): void {
_disposables = new CompositeDisposable(
new Disposable(() => { _taskRunner = null; }),
new Disposable(() => { _initialState = null; }),
atom.commands.add('atom-workspace', {
'nuclide-swift:create-new-package': () =>
_getTaskRunner().runTask('create-new-package'),
'nuclide-swift:fetch-package-dependencies': () =>
_getTaskRunner().runTask('fetch-package-dependencies'),
'nuclide-swift:update-package-dependencies': () =>
_getTaskRunner().runTask('update-package-dependencies'),
'nuclide-swift:generate-xcode-project': () =>
_getTaskRunner().runTask('generate-xcode-project'),
'nuclide-swift:visualize-package-dependencies': () =>
_getTaskRunner().runTask('visualize-package-dependencies'),
'nuclide-swift:display-buffer-description': () =>
_getTaskRunner().runTask('display-buffer-description'),
}),
);
}

Expand Down Expand Up @@ -62,6 +82,12 @@ export function consumeOutputService(service: OutputService): void {
}));
}

export function consumeNuclideSideBar(sideBar: NuclideSideBarService): Disposable {
// FIXME: Add a "Swift Package Tests" sidebar. See
// pkg/nuclide-source-control-side-bar for an example.
return new Disposable(() => {});
}

export function serialize(): ?SwiftPMTaskRunnerStoreState {
if (_taskRunner != null) {
return _taskRunner.serialize();
Expand All @@ -81,6 +107,36 @@ export function createAutocompleteProvider(): atom$AutocompleteProvider {
};
}

export function getOutlineProvider(): OutlineProvider {
return {
grammarScopes: ['source.swift'],
priority: 1,
name: 'Swift',
getOutline,
};
}

export function provideCodeFormat(): CodeFormatProvider {
return {
selector: 'source.swift',
inclusionPriority: 1,
formatEntireFile(editor, range) {
return CodeFormatHelpers.formatEntireFile(editor, range);
},
};
}

export function createTypeHintProvider(): TypeHintProvider {
return {
selector: 'source.swift',
providerName: 'nuclide-swift',
inclusionPriority: 1,
typeHint(editor: TextEditor, position: atom$Point): Promise<?TypeHint> {
return _getTaskRunner().getTypeHintProvider().typeHint(editor, position);
},
};
}

function _getTaskRunner(): SwiftPMTaskRunner {
if (_taskRunner == null) {
invariant(_disposables != null);
Expand Down
2 changes: 1 addition & 1 deletion pkg/nuclide-swift/lib/sourcekitten/SourceKitten.js
Expand Up @@ -16,7 +16,7 @@ import featureConfig from '../../../nuclide-feature-config';
* Commands that SourceKitten implements and nuclide-swift supports, such as
* "complete" for autocompletion.
*/
export type SourceKittenCommand = 'complete';
export type SourceKittenCommand = 'complete' | 'index';

/**
* Returns the path to SourceKitten, based on the user's Nuclide config.
Expand Down
56 changes: 56 additions & 0 deletions pkg/nuclide-swift/lib/taskrunner/SwiftOutlineProvider.js
@@ -0,0 +1,56 @@
'use babel';
/* @flow */

/*
* 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.
*/

import type {Outline, OutlineTree} from '../../../nuclide-outline-view';

import {Point} from 'atom';
import {asyncExecute} from '../../../commons-node/process';
import {arrayCompact} from '../../../commons-node/collection';

export async function getOutline(editor: atom$TextEditor): Promise<?Outline> {
const contents = editor.getText();
const result = await asyncExecute(
// TODO use the setting for sourcekitten path
'sourcekitten',
[
'structure',
'--text', contents,
// TODO add compiler args?
],
);

// TODO handle errors
return outputToOutline(result.stdout);
}

function outputToOutline(output: string): Outline {
const json = JSON.parse(output);

const outlineTrees: Array<?OutlineTree> = json['key.substructure'].map(itemToOutline);
return {
outlineTrees: arrayCompact(outlineTrees),
};
}

function itemToOutline(item: Object): ?OutlineTree {
// TODO support more things
switch (item['key.kind']) {
case 'source.lang.swift.decl.var.global':
return {
plainText: item['key.name'],
// TODO get the actual start position
startPosition: new Point(0, 0),
children: [],
};
default:
return null;
}
}
73 changes: 71 additions & 2 deletions pkg/nuclide-swift/lib/taskrunner/SwiftPMTaskRunner.js
Expand Up @@ -24,14 +24,25 @@ import {observeProcess, safeSpawn} from '../../../commons-node/process';
import {observableToTaskInfo} from '../../../commons-node/observableToTaskInfo';
import SwiftPMTaskRunnerStore from './SwiftPMTaskRunnerStore';
import SwiftPMTaskRunnerActions from './SwiftPMTaskRunnerActions';
import {buildCommand, testCommand} from './SwiftPMTaskRunnerCommands';
import {
buildCommand,
testCommand,
createNewPackageCommand,
fetchPackageDependenciesCommand,
updatePackageDependenciesCommand,
generateXcodeProjectCommand,
visualizePackageDependenciesCommand,
displayBufferDescriptionCommand,
} from './SwiftPMTaskRunnerCommands';
import {
SwiftPMTaskRunnerBuildTask,
SwiftPMTaskRunnerTestTask,
SwiftPMTaskRunnerTasks,
} from './SwiftPMTaskRunnerTasks';
import SwiftPMTaskRunnerToolbar from './toolbar/SwiftPMTaskRunnerToolbar';
import SwiftPMAutocompletionProvider from './providers/SwiftPMAutocompletionProvider';
import SwiftPMTypeHintProvider from './typehint/SwiftPMTypeHintProvider';
import {addTestResultGutterIcon, highlightLine} from './gutter/TestResults.js';
import {SwiftIcon} from '../ui/SwiftIcon';

/**
Expand All @@ -54,6 +65,7 @@ export class SwiftPMTaskRunner {
_disposables: CompositeDisposable;
_store: SwiftPMTaskRunnerStore;
_autocompletionProvier: SwiftPMAutocompletionProvider;
_typeHintProvider: SwiftPMTypeHintProvider;
_actions: SwiftPMTaskRunnerActions;
_tasks: Observable<Array<Task>>;
_outputMessages: Subject<Message>;
Expand All @@ -67,10 +79,17 @@ export class SwiftPMTaskRunner {
this._actions = new SwiftPMTaskRunnerActions(dispatcher);
this._outputMessages = new Subject();
this._autocompletionProvier = new SwiftPMAutocompletionProvider(this._store);
this._typeHintProvider = new SwiftPMTypeHintProvider(this._store);

this._disposables = new CompositeDisposable();
this._disposables.add(this._store);
this._disposables.add(this._outputMessages);
this._disposables.add(atom.workspace.observeTextEditors(editor => {
editor.addGutter({
name: 'nuclide-swift-test-result',
visible: false,
});
}));
}

dispose(): void {
Expand Down Expand Up @@ -134,9 +153,24 @@ export class SwiftPMTaskRunner {
case SwiftPMTaskRunnerTestTask.type:
command = testCommand(chdir, buildPath);
break;
case 'create-new-package':
command = createNewPackageCommand(this._store);
break;
case 'fetch-package-dependencies':
command = fetchPackageDependenciesCommand(this._store);
break;
case 'update-package-dependencies':
command = updatePackageDependenciesCommand(this._store);
break;
case 'generate-xcode-project':
command = generateXcodeProjectCommand(this._store);
break;
case 'visualize-package-dependencies':
command = visualizePackageDependenciesCommand(this._store);
break;
default:
// FIXME: Throw an error for unknown task types.
command = testCommand(chdir, buildPath);
command = displayBufferDescriptionCommand(this._store);
break;
}

Expand All @@ -150,6 +184,10 @@ export class SwiftPMTaskRunner {
case 'stderr':
case 'stdout':
this._logOutput(message.data, 'log');
// FIXME: These test failures are displayed but never removed.
// nuclide-swift should cache a list of files/lines for test
// failures, and remove test failures before each task is run.
this._showTestFailure(message.data);
break;
case 'exit':
this._logOutput(`Exited with exit code ${message.exitCode}`, 'log');
Expand Down Expand Up @@ -195,7 +233,38 @@ export class SwiftPMTaskRunner {
return this._autocompletionProvier;
}

getTypeHintProvider(): SwiftPMTypeHintProvider {
return this._typeHintProvider;
}

_logOutput(text: string, level: Level) {
this._outputMessages.next({text, level});
}

async _showTestFailure(text: string) {
if (!text.includes('error:')) {
return;
}
const components = text.split(':');
if (components.length < 2) {
return;
}

const path = components[0];
const fileExists = await fsPromise.exists(path);
if (fileExists) {
for (const editor of atom.workspace.getTextEditors()) {
if (editor.getBuffer().getPath() === path) {
const line = parseInt(components[1], 10) - 1;
addTestResultGutterIcon(editor, line, 'nuclide-swift-test-failed-icon');
highlightLine(editor, line);
}
}
} else {
atom.notifications.addError('Error', {
detail: text.substring(text.indexOf('error:') + 7),
dismissable: true,
});
}
}
}

0 comments on commit b68e0d8

Please sign in to comment.