diff --git a/flow-libs/atom.js.flow b/flow-libs/atom.js.flow index a0c1a386..c9ca8232 100644 --- a/flow-libs/atom.js.flow +++ b/flow-libs/atom.js.flow @@ -1028,7 +1028,7 @@ declare class atom$TextEditor extends atom$Model { onDidConflict(callback: () => void): IDisposable, serialize(): Object, foldBufferRowRange(startRow: number, endRow: number): void, - getNonWordCharacters(scope?: atom$ScopeDescriptor): string, + getNonWordCharacters(position?: atom$PointLike): string, } /** @@ -2049,6 +2049,9 @@ type atom$AutocompleteProvider = { +getSuggestions: ( request: atom$AutocompleteRequest, ) => Promise> | ?Array, + +getSuggestionDetailsOnSelect?: ( + suggestion: atom$AutocompleteSuggestion + ) => Promise, +disableForSelector?: string, +inclusionPriority?: number, +excludeLowerPriority?: boolean, diff --git a/flow-libs/vscode-debugprotocol.js.flow b/flow-libs/vscode-debugprotocol.js.flow index c70bb530..8ecae116 100644 --- a/flow-libs/vscode-debugprotocol.js.flow +++ b/flow-libs/vscode-debugprotocol.js.flow @@ -1075,6 +1075,10 @@ declare module 'vscode-debugprotocol' { supportsExceptionInfoRequest?: boolean, /** The debug adapter supports the 'terminateDebuggee' attribute on the 'disconnect' request. */ supportTerminateDebuggee?: boolean, + /** The debug adapter supports custom `continueToLocation` logic. + * This is not part of the standard Visual Studio Code debug protocol. + */ + supportsContinueToLocation?: boolean, }; /** An ExceptionBreakpointsFilter is shown in the UI as an option for configuring how exceptions are dealt with. */ diff --git a/modules/atom-ide-ui/pkg/atom-ide-console/lib/main.js b/modules/atom-ide-ui/pkg/atom-ide-console/lib/main.js index 28d247e9..1eda6a26 100644 --- a/modules/atom-ide-ui/pkg/atom-ide-console/lib/main.js +++ b/modules/atom-ide-ui/pkg/atom-ide-console/lib/main.js @@ -231,6 +231,9 @@ class Activation { info(object: string): void { console.append({text: object, level: 'info'}); }, + success(object: string): void { + console.append({text: object, level: 'success'}); + }, append(message: Message): void { invariant(activation != null && !disposed); activation._getStore().dispatch( @@ -328,7 +331,12 @@ class Activation { records: this._store .getState() .records.slice(-maximumSerializedMessages) - .toArray(), + .toArray() + .map(record => { + // `Executor` is not serializable. Make sure to remove it first. + const {executor, ...rest} = record; + return rest; + }), history: this._store.getState().history.slice(-maximumSerializedHistory), }; } diff --git a/modules/atom-ide-ui/pkg/atom-ide-console/lib/parseText.js b/modules/atom-ide-ui/pkg/atom-ide-console/lib/parseText.js new file mode 100644 index 00000000..19ec535e --- /dev/null +++ b/modules/atom-ide-ui/pkg/atom-ide-console/lib/parseText.js @@ -0,0 +1,111 @@ +/** + * Copyright (c) 2017-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @flow + * @format + */ + +import {goToLocation} from 'nuclide-commons-atom/go-to-location'; +import * as React from 'react'; +import featureConfig from 'nuclide-commons-atom/feature-config'; + +import {URL_REGEX} from 'nuclide-commons/string'; +const DIFF_PATTERN = '\\b[dD][1-9][0-9]{5,}\\b'; +const TASK_PATTERN = '\\b[tT]\\d+\\b'; +const FILE_PATH_PATTERN = + '([/A-Za-z_-s0-9.-]+[.][A-Za-z]+)(:([0-9]+))?(:([0-9]+))?'; +const CLICKABLE_PATTERNS = `(${DIFF_PATTERN})|(${TASK_PATTERN})|(${ + URL_REGEX.source +})|${FILE_PATH_PATTERN}`; +const CLICKABLE_RE = new RegExp(CLICKABLE_PATTERNS, 'g'); + +function toString(value: mixed): string { + return typeof value === 'string' ? value : ''; +} + +/** + * Parse special entities into links. In the future, it would be great to add a service so that we + * could add new clickable things and to allow providers to mark specific ranges as links to things + * that only they can know (e.g. relative paths output in BUCK messages). For now, however, we'll + * just use some pattern settings and hardcode the patterns we care about. + */ +export default function parseText( + text: string, +): Array> { + const chunks = []; + let lastIndex = 0; + let index = 0; + while (true) { + const match = CLICKABLE_RE.exec(text); + if (match == null) { + break; + } + + const matchedText = match[0]; + + // Add all the text since our last match. + chunks.push( + text.slice(lastIndex, CLICKABLE_RE.lastIndex - matchedText.length), + ); + lastIndex = CLICKABLE_RE.lastIndex; + + let href; + let handleOnClick; + if (match[1] != null) { + // It's a diff + const url = toString( + featureConfig.get('atom-ide-console.diffUrlPattern'), + ); + if (url !== '') { + href = url.replace('%s', matchedText); + } + } else if (match[2] != null) { + // It's a task + const url = toString( + featureConfig.get('atom-ide-console.taskUrlPattern'), + ); + if (url !== '') { + href = url.replace('%s', matchedText.slice(1)); + } + } else if (match[3] != null) { + // It's a URL + href = matchedText; + } else if (match[5] != null) { + // It's a file path + href = '#'; + handleOnClick = () => { + goToLocation(match[5], { + line: match[7] ? parseInt(match[7], 10) - 1 : 0, + column: match[9] ? parseInt(match[9], 10) - 1 : 0, + }); + }; + } + + chunks.push( + // flowlint-next-line sketchy-null-string:off + href ? ( + + {matchedText} + + ) : ( + matchedText + ), + ); + + index++; + } + + // Add any remaining text. + chunks.push(text.slice(lastIndex)); + + return chunks; +} diff --git a/modules/atom-ide-ui/pkg/atom-ide-console/lib/types.js b/modules/atom-ide-ui/pkg/atom-ide-console/lib/types.js index 812478f2..cd5a52d8 100644 --- a/modules/atom-ide-ui/pkg/atom-ide-console/lib/types.js +++ b/modules/atom-ide-ui/pkg/atom-ide-console/lib/types.js @@ -27,6 +27,7 @@ export type ConsoleApi = { error(object: string, _: void): void, warn(object: string, _: void): void, info(object: string, _: void): void, + success(object: string, _: void): void, // A generic API for sending a message of any level (log, error, etc.). append(message: Message): void, diff --git a/modules/atom-ide-ui/pkg/atom-ide-console/lib/ui/RecordView.js b/modules/atom-ide-ui/pkg/atom-ide-console/lib/ui/RecordView.js index e3a0ed80..d5741f62 100644 --- a/modules/atom-ide-ui/pkg/atom-ide-console/lib/ui/RecordView.js +++ b/modules/atom-ide-ui/pkg/atom-ide-console/lib/ui/RecordView.js @@ -27,8 +27,7 @@ import shallowEqual from 'shallowequal'; import {TextRenderer} from 'nuclide-commons-ui/TextRenderer'; import debounce from 'nuclide-commons/debounce'; import {nextAnimationFrame} from 'nuclide-commons/observable'; -import {URL_REGEX} from 'nuclide-commons/string'; -import featureConfig from 'nuclide-commons-atom/feature-config'; +import parseText from '../parseText'; type Props = { displayableRecord: DisplayableRecord, @@ -238,80 +237,3 @@ function getIconName(record: Record): ?string { return 'stop'; } } - -/** - * Parse special entities into links. In the future, it would be great to add a service so that we - * could add new clickable things and to allow providers to mark specific ranges as links to things - * that only they can know (e.g. relative paths output in BUCK messages). For now, however, we'll - * just use some pattern settings and hardcode the patterns we care about. - */ -function parseText(text: string): Array> { - const chunks = []; - let lastIndex = 0; - let index = 0; - while (true) { - const match = CLICKABLE_RE.exec(text); - if (match == null) { - break; - } - - const matchedText = match[0]; - - // Add all the text since our last match. - chunks.push( - text.slice(lastIndex, CLICKABLE_RE.lastIndex - matchedText.length), - ); - lastIndex = CLICKABLE_RE.lastIndex; - - let href; - if (match[1] != null) { - // It's a diff - const url = toString( - featureConfig.get('atom-ide-console.diffUrlPattern'), - ); - if (url !== '') { - href = url.replace('%s', matchedText); - } - } else if (match[2] != null) { - // It's a task - const url = toString( - featureConfig.get('atom-ide-console.taskUrlPattern'), - ); - if (url !== '') { - href = url.replace('%s', matchedText.slice(1)); - } - } else if (match[3] != null) { - // It's a URL - href = matchedText; - } - - chunks.push( - // flowlint-next-line sketchy-null-string:off - href ? ( - - {matchedText} - - ) : ( - matchedText - ), - ); - - index++; - } - - // Add any remaining text. - chunks.push(text.slice(lastIndex)); - - return chunks; -} - -const DIFF_PATTERN = '\\b[dD][1-9][0-9]{5,}\\b'; -const TASK_PATTERN = '\\b[tT]\\d+\\b'; -const CLICKABLE_PATTERNS = `(${DIFF_PATTERN})|(${TASK_PATTERN})|${ - URL_REGEX.source -}`; -const CLICKABLE_RE = new RegExp(CLICKABLE_PATTERNS, 'g'); - -function toString(value: mixed): string { - return typeof value === 'string' ? value : ''; -} diff --git a/modules/atom-ide-ui/pkg/atom-ide-console/spec/parseText-spec.js b/modules/atom-ide-ui/pkg/atom-ide-console/spec/parseText-spec.js new file mode 100644 index 00000000..9a878e7b --- /dev/null +++ b/modules/atom-ide-ui/pkg/atom-ide-console/spec/parseText-spec.js @@ -0,0 +1,137 @@ +/** + * Copyright (c) 2017-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @flow + * @format + */ + +import parseText from '../lib/parseText'; + +describe('parseText', () => { + it('parses url pattern', () => { + const chunks = parseText('Message: https://facebook.com'); + expect(chunks.length).toBe(3); + expect(chunks[0]).toBe('Message: '); + expect(chunks[2]).toBe(''); + + const reactElement = chunks[1]; + expect(typeof reactElement).toBe('object'); // type React.Element + + if (typeof reactElement === 'object') { + expect(reactElement.type).toBe('a'); + expect(reactElement.props.href).toBe('https://facebook.com'); + expect(reactElement.props.children).toBe('https://facebook.com'); + } + }); + + it('parses absolute file path', () => { + const chunks = parseText( + 'Message: /absolute/file/path/file.js should be parsed.', + ); + expect(chunks.length).toBe(3); + expect(chunks[0]).toBe('Message: '); + expect(chunks[2]).toBe(' should be parsed.'); + + const reactElement = chunks[1]; + expect(typeof reactElement).toBe('object'); // type React.Element + + if (typeof reactElement === 'object') { + expect(reactElement.type).toBe('a'); + expect(reactElement.props.onClick).toBeDefined(); + expect(reactElement.props.children).toBe('/absolute/file/path/file.js'); + } + }); + + it('parses absolute file path with line number', () => { + const chunks = parseText( + 'Message: /absolute/file/path/file.js:10 should be parsed.', + ); + expect(chunks.length).toBe(3); + expect(chunks[0]).toBe('Message: '); + expect(chunks[2]).toBe(' should be parsed.'); + + const reactElement = chunks[1]; + expect(typeof reactElement).toBe('object'); // type React.Element + + if (typeof reactElement === 'object') { + expect(reactElement.type).toBe('a'); + expect(reactElement.props.onClick).toBeDefined(); + expect(reactElement.props.children).toBe( + '/absolute/file/path/file.js:10', + ); + } + }); + + it('parses absolute file path with line number and column number', () => { + const chunks = parseText( + 'Message: /absolute/file/path/file.js:1:17 should be parsed.', + ); + expect(chunks.length).toBe(3); + expect(chunks[0]).toBe('Message: '); + expect(chunks[2]).toBe(' should be parsed.'); + + const reactElement = chunks[1]; + expect(typeof reactElement).toBe('object'); // type React.Element + + if (typeof reactElement === 'object') { + expect(reactElement.type).toBe('a'); + expect(reactElement.props.onClick).toBeDefined(); + expect(reactElement.props.children).toBe( + '/absolute/file/path/file.js:1:17', + ); + } + }); + + it('parses relative file path', () => { + const chunks = parseText('relative/path/file.js:1:17 should be parsed.'); + expect(chunks.length).toBe(3); + expect(chunks[0]).toBe(''); + expect(chunks[2]).toBe(' should be parsed.'); + + const reactElement = chunks[1]; + expect(typeof reactElement).toBe('object'); // type React.Element + + if (typeof reactElement === 'object') { + expect(reactElement.type).toBe('a'); + expect(reactElement.props.onClick).toBeDefined(); + expect(reactElement.props.children).toBe('relative/path/file.js:1:17'); + } + }); + + it('parses mutliple file paths', () => { + const chunks = parseText( + 'Message: relative/path/file.js:1:17 and /Absolute/path/file.file.js should be parsed.', + ); + expect(chunks.length).toBe(5); + expect(chunks[0]).toBe('Message: '); + expect(chunks[2]).toBe(' and '); + expect(chunks[4]).toBe(' should be parsed.'); + + const reactElementRelative = chunks[1]; + const reactElementAbsolute = chunks[3]; + expect(typeof reactElementRelative).toBe('object'); + expect(typeof reactElementAbsolute).toBe('object'); + + if ( + typeof reactElementRelative === 'object' && + typeof reactElementAbsolute === 'object' + ) { + expect(reactElementRelative.type).toBe('a'); + expect(reactElementRelative.props.onClick).toBeDefined(); + expect(reactElementRelative.props.children).toBe( + 'relative/path/file.js:1:17', + ); + + expect(reactElementAbsolute.type).toBe('a'); + expect(reactElementAbsolute.props.onClick).toBeDefined(); + expect(reactElementAbsolute.props.children).toBe( + '/Absolute/path/file.file.js', + ); + } + }); +}); diff --git a/modules/atom-ide-ui/pkg/atom-ide-global/lib/backports/installTextEditorStyles.js b/modules/atom-ide-ui/pkg/atom-ide-global/lib/backports/installTextEditorStyles.js new file mode 100644 index 00000000..d1dbc4b8 --- /dev/null +++ b/modules/atom-ide-ui/pkg/atom-ide-global/lib/backports/installTextEditorStyles.js @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2017-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @flow + * @format + */ + +import UniversalDisposable from 'nuclide-commons/UniversalDisposable'; +import {observableFromSubscribeFunction} from 'nuclide-commons/event'; +import {Observable} from 'rxjs'; +import invariant from 'assert'; +import semver from 'semver'; + +export default function installTextEditorStyles(): IDisposable { + if (semver.gte(atom.appVersion, '1.26.0')) { + // this behavior is part of 1.26 and greater + return new UniversalDisposable(); + } + + let styleSheetDisposable = new UniversalDisposable(); + + return new UniversalDisposable( + () => styleSheetDisposable.dispose(), + Observable.combineLatest( + observableFromSubscribeFunction( + atom.config.observe.bind(atom.config, 'editor.fontSize'), + ), + observableFromSubscribeFunction( + atom.config.observe.bind(atom.config, 'editor.fontFamily'), + ), + observableFromSubscribeFunction( + atom.config.observe.bind(atom.config, 'editor.lineHeight'), + ), + ).subscribe(([fontSize, fontFamily, lineHeight]) => { + invariant( + typeof fontSize === 'number' && + typeof fontFamily === 'string' && + typeof lineHeight === 'number', + ); + + const styleSheetSource = ` + atom-workspace { + --editor-font-size: ${fontSize}px; + --editor-font-family: ${fontFamily}; + --editor-line-height: ${lineHeight}; + } + `; + + styleSheetDisposable.dispose(); + // $FlowIgnore + styleSheetDisposable = atom.workspace.styleManager.addStyleSheet( + styleSheetSource, + { + sourcePath: 'text-editor-styles-backport', + priority: -1, + }, + ); + }), + ); +} diff --git a/modules/atom-ide-ui/pkg/atom-ide-global/lib/main.js b/modules/atom-ide-ui/pkg/atom-ide-global/lib/main.js new file mode 100644 index 00000000..148ce23e --- /dev/null +++ b/modules/atom-ide-ui/pkg/atom-ide-global/lib/main.js @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2017-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @flow + * @format + */ + +import createPackage from 'nuclide-commons-atom/createPackage'; +import UniversalDisposable from 'nuclide-commons/UniversalDisposable'; +import installTextEditorStyles from './backports/installTextEditorStyles'; + +class Activation { + _disposables: UniversalDisposable; + + activate() { + this._disposables = new UniversalDisposable(installTextEditorStyles()); + } + + dispose() { + this._disposables.dispose(); + } +} + +createPackage(module.exports, Activation); diff --git a/modules/atom-ide-ui/pkg/atom-ide-outline-view/lib/OutlineView.js b/modules/atom-ide-ui/pkg/atom-ide-outline-view/lib/OutlineView.js index 8c5d554e..7854b731 100644 --- a/modules/atom-ide-ui/pkg/atom-ide-outline-view/lib/OutlineView.js +++ b/modules/atom-ide-ui/pkg/atom-ide-outline-view/lib/OutlineView.js @@ -65,40 +65,15 @@ const TOKEN_KIND_TO_CLASS_NAME_MAP = { }; export class OutlineView extends React.PureComponent { - subscription: ?UniversalDisposable; _outlineViewRef: ?React.ElementRef; - state = { - fontFamily: (atom.config.get('editor.fontFamily'): any), - fontSize: (atom.config.get('editor.fontSize'): any), - lineHeight: (atom.config.get('editor.lineHeight'): any), - }; componentDidMount(): void { - invariant(this.subscription == null); - this.subscription = new UniversalDisposable( - atom.config.observe('editor.fontSize', (size: mixed) => { - this.setState({fontSize: (size: any)}); - }), - atom.config.observe('editor.fontFamily', (font: mixed) => { - this.setState({fontFamily: (font: any)}); - }), - atom.config.observe('editor.lineHeight', (size: mixed) => { - this.setState({lineHeight: (size: any)}); - }), - ); - // Ensure that focus() gets called during the initial mount. if (this.props.visible) { this.focus(); } } - componentWillUnmount(): void { - invariant(this.subscription != null); - this.subscription.unsubscribe(); - this.subscription = null; - } - componentDidUpdate(prevProps: Props) { if (this.props.visible && !prevProps.visible) { this.focus(); @@ -120,17 +95,6 @@ export class OutlineView extends React.PureComponent { render(): React.Node { return (
-