Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

show lightbulb inline #33682

Merged
merged 4 commits into from
Sep 4, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/vs/editor/contrib/quickFix/browser/lightBulbWidget.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
align-items: center;
justify-content: center;
height: 16px;
width: 16px;
width: 20px;
padding-left: 2px;
}

.monaco-editor .lightbulb-glyph.hidden {
Expand All @@ -18,6 +19,7 @@

.monaco-editor .lightbulb-glyph:hover {
cursor: pointer;
transform: scale(1.3, 1.3);
}

.monaco-editor.vs .lightbulb-glyph {
Expand Down
122 changes: 76 additions & 46 deletions src/vs/editor/contrib/quickFix/browser/lightBulbWidget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,72 +6,86 @@

import 'vs/css!./lightBulbWidget';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { IDisposable } from 'vs/base/common/lifecycle';
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
import Event, { Emitter } from 'vs/base/common/event';
import * as dom from 'vs/base/browser/dom';
import { TrackedRangeStickiness } from 'vs/editor/common/editorCommon';
import { ICodeEditor, MouseTargetType } from 'vs/editor/browser/editorBrowser';
import { ICodeEditor, IContentWidget, IContentWidgetPosition, ContentWidgetPositionPreference } from 'vs/editor/browser/editorBrowser';
import { QuickFixComputeEvent } from './quickFixModel';
import { IMarkdownString } from 'vs/base/common/htmlContent';

export class LightBulbWidget implements IDisposable {
export class LightBulbWidget implements IDisposable, IContentWidget {

private readonly _options = {
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
glyphMarginClassName: 'lightbulb-glyph',
glyphMarginHoverMessage: <IMarkdownString>undefined
};
private static readonly _posPref = [ContentWidgetPositionPreference.EXACT];

private readonly _domNode: HTMLDivElement;
private readonly _editor: ICodeEditor;
private readonly _disposables: IDisposable[] = [];
private readonly _onClick = new Emitter<{ x: number, y: number }>();
private readonly _mouseDownSubscription: IDisposable;

private _decorationIds: string[] = [];
private _currentLine: number;
readonly onClick: Event<{ x: number, y: number }> = this._onClick.event;

private _position: IContentWidgetPosition;
private _model: QuickFixComputeEvent;
private _futureFixes = new CancellationTokenSource();

constructor(editor: ICodeEditor) {
this._editor = editor;
this._mouseDownSubscription = this._editor.onMouseDown(e => {

// not on glyh margin or not on 💡
if (e.target.type !== MouseTargetType.GUTTER_GLYPH_MARGIN
|| this._currentLine === undefined
|| this._currentLine !== e.target.position.lineNumber
) {
return;
}
this._editor.addContentWidget(this);

this._domNode = document.createElement('div');
this._domNode.className = 'lightbulb-glyph';

this._disposables.push(dom.addStandardDisposableListener(this._domNode, 'click', e => {
// a bit of extra work to make sure the menu
// doesn't cover the line-text
const { top, height } = dom.getDomNodePagePosition(<HTMLDivElement>e.target.element);
const { top, height } = dom.getDomNodePagePosition(this._domNode);
const { lineHeight } = this._editor.getConfiguration();

let pad = Math.floor(lineHeight / 3);
if (this._position.position.lineNumber < this._model.position.lineNumber) {
pad += lineHeight;
}

this._onClick.fire({
x: e.event.posx,
y: top + height + Math.floor(lineHeight / 3)
x: e.posx,
y: top + height + pad
});
});
}));

this._disposables.push(this._editor.onDidChangeCursorSelection(e => {
// hide lightbulb when selection starts to
// enclose it
if (this._position && e.selection.containsPosition(this._position.position)) {
this.hide();
}
}));
}

dispose(): void {
this._mouseDownSubscription.dispose();
this.hide();
dispose(this._disposables);
this._editor.removeContentWidget(this);
}

get onClick(): Event<{ x: number, y: number }> {
return this._onClick.event;
getId(): string {
return 'LightBulbWidget';
}

set model(e: QuickFixComputeEvent) {
this._model = e;
getDomNode(): HTMLElement {
return this._domNode;
}

getPosition(): IContentWidgetPosition {
return this._position;
}

set model(value: QuickFixComputeEvent) {
this.hide();
this._model = value;
this._futureFixes = new CancellationTokenSource();
const { token } = this._futureFixes;

e.fixes.done(fixes => {
this._model.fixes.done(fixes => {
if (!token.isCancellationRequested && fixes && fixes.length > 0) {
this.show(e);
this.show(this._model);
} else {
this.hide();
}
Expand All @@ -85,26 +99,42 @@ export class LightBulbWidget implements IDisposable {
}

set title(value: string) {
// TODO(joh,alex) this isn't working well because the hover hover
// message sticks around after clicking the light bulb
// this._options.glyphMarginHoverMessage = value;
this._domNode.title = value;
}

get title() {
return this._options.glyphMarginHoverMessage && this._options.glyphMarginHoverMessage.value;
get title(): string {
return this._domNode.title;
}

show(e: QuickFixComputeEvent): void {
this._currentLine = e.range.startLineNumber;
this._decorationIds = this._editor.deltaDecorations(this._decorationIds, [{
options: this._options,
range: { ...e.range, endLineNumber: e.range.startLineNumber }
}]);
const { fontInfo } = this._editor.getConfiguration();
const { lineNumber } = e.position;
const model = this._editor.getModel();
const indent = model.getIndentLevel(lineNumber);
const lineHasSpace = fontInfo.spaceWidth * indent > 22;

let effectiveLineNumber = lineNumber;
if (!lineHasSpace) {
if (lineNumber > 1) {
effectiveLineNumber -= 1;
} else {
effectiveLineNumber += 1;
}
}

this._position = {
position: { lineNumber: effectiveLineNumber, column: 1 },
preference: LightBulbWidget._posPref
};

this._editor.layoutContentWidget(this);
this._model = e;
}

hide(): void {
this._decorationIds = this._editor.deltaDecorations(this._decorationIds, []);
this._position = null;
this._model = null;
this._futureFixes.cancel();
this._currentLine = undefined;
this._editor.layoutContentWidget(this);
}
}
60 changes: 33 additions & 27 deletions src/vs/editor/contrib/quickFix/browser/quickFixModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,9 @@ import { CodeActionProviderRegistry, Command } from 'vs/editor/common/modes';
import { getCodeActions } from './quickFix';
import { Position } from 'vs/editor/common/core/position';


export class QuickFixOracle {

private _disposables: IDisposable[] = [];
private _currentRange: Range;

constructor(
private _editor: ICommonCodeEditor,
Expand All @@ -39,35 +37,25 @@ export class QuickFixOracle {
}

trigger(type: 'manual' | 'auto'): void {

// get selection from marker or current word
// unless the selection is non-empty and manually
// requesting code actions
const range = (type === 'manual' && this._getRangeOfNonEmptySelection())
|| this._getRangeOfMarker()
|| this._getRangeOfWord()
|| this._editor.getSelection();

this._createEventAndSignalChange(type, range);
let rangeOrSelection = this._getRangeOfMarker() || this._getRangeOfSelectionUnlessWhitespaceEnclosed();
if (type === 'manual') {
rangeOrSelection = this._editor.getSelection();
}
this._createEventAndSignalChange(type, rangeOrSelection);
}

private _onMarkerChanges(resources: URI[]): void {
const { uri } = this._editor.getModel();
for (const resource of resources) {
if (resource.toString() === uri.toString()) {
this._currentRange = undefined;
this._onCursorChange();
this.trigger('auto');
return;
}
}
}

private _onCursorChange(): void {
const rangeOrSelection = this._getRangeOfMarker() || this._getRangeOfNonEmptySelection();
if (!Range.equalsRange(this._currentRange, rangeOrSelection)) {
this._currentRange = rangeOrSelection;
this._createEventAndSignalChange('auto', rangeOrSelection);
}
this.trigger('auto');
}

private _getRangeOfMarker(): Range {
Expand All @@ -81,15 +69,33 @@ export class QuickFixOracle {
return undefined;
}

private _getRangeOfWord(): Range {
const pos = this._editor.getPosition();
const info = this._editor.getModel().getWordAtPosition(pos);
return info ? new Range(pos.lineNumber, info.startColumn, pos.lineNumber, info.endColumn) : undefined;
}

private _getRangeOfNonEmptySelection(): Selection {
private _getRangeOfSelectionUnlessWhitespaceEnclosed(): Selection {
const model = this._editor.getModel();
const selection = this._editor.getSelection();
return !selection.isEmpty() ? selection : undefined;
if (selection.isEmpty()) {
const { lineNumber, column } = selection.getPosition();
const line = model.getLineContent(lineNumber);
if (line.length === 0) {
// empty line
return undefined;
} else if (column === 1) {
// look only right
if (/\s/.test(line[0])) {
return undefined;
}
} else if (column === model.getLineMaxColumn(lineNumber)) {
// look only left
if (/\s/.test(line[line.length - 1])) {
return undefined;
}
} else {
// look left and right
if (/\s/.test(line[column - 2]) && /\s/.test(line[column - 1])) {
return undefined;
}
}
}
return selection;
}

private _createEventAndSignalChange(type: 'auto' | 'manual', rangeOrSelection: Range | Selection): void {
Expand Down