diff --git a/news/1 Enhancements/4111.md b/news/1 Enhancements/4111.md new file mode 100644 index 000000000000..ee8423ee8e1a --- /dev/null +++ b/news/1 Enhancements/4111.md @@ -0,0 +1 @@ +Watermark for Python Interactive input prompt diff --git a/package.nls.json b/package.nls.json index 2b8d494e54ca..cccdccf610ea 100644 --- a/package.nls.json +++ b/package.nls.json @@ -120,6 +120,7 @@ "DataScience.pythonVersionHeaderNoPyKernel": "Python version may not match, no ipykernel found:", "DataScience.pythonRestartHeader": "Restarted Kernel:", "DataScience.executingCodeFailure" : "Executing code failed : {0}", + "DataScience.inputWatermark" : "Shift-enter to run", "Linter.InstalledButNotEnabled": "Linter {0} is installed but not enabled.", "Linter.replaceWithSelectedLinter": "Multiple linters are enabled in settings. Replace with '{0}'?", "DataScience.jupyterSelectURILaunchLocal": "Launch a local Jupyter server when needed", diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 0a505c719781..9279d6a26e82 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -118,6 +118,7 @@ export namespace DataScience { export const pythonInterruptFailedHeader = localize('DataScience.pythonInterruptFailedHeader', 'Keyboard interrupt crashed the kernel. Kernel restarted.'); export const sysInfoURILabel = localize('DataScience.sysInfoURILabel', 'Jupyter Server URI: '); export const executingCodeFailure = localize('DataScience.executingCodeFailure', 'Executing code failed : {0}'); + export const inputWatermark = localize('DataScience.inputWatermark', 'Shift-enter to run'); } export namespace DebugConfigurationPrompts { diff --git a/src/datascience-ui/history-react/MainPanel.tsx b/src/datascience-ui/history-react/MainPanel.tsx index 16da1821c8e3..a50f94cd1640 100644 --- a/src/datascience-ui/history-react/MainPanel.tsx +++ b/src/datascience-ui/history-react/MainPanel.tsx @@ -37,7 +37,7 @@ export class MainPanel extends React.Component super(props); // Default state should show a busy message - this.state = { cellVMs: [], busy: true, undoStack: [], redoStack : [], historyStack: []}; + this.state = { cellVMs: [], busy: true, undoStack: [], redoStack : [], historyStack: [], submittedText: false}; // Add test state if necessary if (!this.props.skipDefault) { @@ -213,6 +213,7 @@ export class MainPanel extends React.Component submitNewCode={this.submitInput} baseTheme={this.props.baseTheme} codeTheme={this.props.codeTheme} + showWatermark={!this.state.submittedText} gotoCode={() => this.gotoCellCode(index)} delete={() => this.deleteCell(index)}/> @@ -637,7 +638,8 @@ export class MainPanel extends React.Component undoStack : this.pushStack(this.state.undoStack, this.state.cellVMs), redoStack: this.state.redoStack, skipNextScroll: false, - historyStack: newHistory + historyStack: newHistory, + submittedText: true }); // Send a message to execute this code if necessary. diff --git a/src/datascience-ui/history-react/cell.tsx b/src/datascience-ui/history-react/cell.tsx index 9288e35178de..09ee8a670aeb 100644 --- a/src/datascience-ui/history-react/cell.tsx +++ b/src/datascience-ui/history-react/cell.tsx @@ -35,6 +35,7 @@ interface ICellProps { autoFocus: boolean; maxTextSize?: number; history: string []; + showWatermark: boolean; gotoCode(): void; delete(): void; submitNewCode(code: string): void; @@ -205,6 +206,7 @@ export class Cell extends React.Component { codeTheme={this.props.codeTheme} testMode={this.props.testMode ? true : false} readOnly={!this.props.cellVM.editable} + showWatermark={this.props.showWatermark} onSubmit={this.props.submitNewCode} onChangeLineCount={this.onChangeLineCount} ref={this.updateCodeRef} diff --git a/src/datascience-ui/history-react/code.css b/src/datascience-ui/history-react/code.css index 08aadb31c45d..47a124abefa6 100644 --- a/src/datascience-ui/history-react/code.css +++ b/src/datascience-ui/history-react/code.css @@ -36,3 +36,17 @@ .code-area-editable { margin-bottom: 10px; } + +.code-watermark { + position: absolute; + top: 0; + left: 30px; + z-index: 500; + font-style: italic; + color: var(--vscode-pickerGroup-border); +} + +.hide { + visibility: hidden; +} + diff --git a/src/datascience-ui/history-react/code.tsx b/src/datascience-ui/history-react/code.tsx index 5d6f97b3c5e6..7a70c8b8334f 100644 --- a/src/datascience-ui/history-react/code.tsx +++ b/src/datascience-ui/history-react/code.tsx @@ -1,249 +1,259 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; - -import 'codemirror/lib/codemirror.css'; -import 'codemirror/mode/python/python'; - -import * as CodeMirror from 'codemirror'; -import * as React from 'react'; -import * as RCM from 'react-codemirror'; - -import './code.css'; - -import { Cursor } from './cursor'; -import { InputHistory } from './inputHistory'; - -export interface ICodeProps { - autoFocus: boolean; - code : string; - codeTheme: string; - testMode: boolean; - readOnly: boolean; - history: string[]; - cursorType: string; - onSubmit(code: string): void; - onChangeLineCount(lineCount: number) : void; - -} - -interface ICodeState { - focused: boolean; - cursorLeft: number; - cursorTop: number; - cursorBottom: number; - charUnderCursor: string; -} - -export class Code extends React.Component { - - private codeMirror: CodeMirror.Editor | undefined; - private history : InputHistory; - private baseIndentation : number | undefined; - - constructor(prop: ICodeProps) { - super(prop); - this.state = {focused: false, cursorLeft: 0, cursorTop: 0, cursorBottom: 0, charUnderCursor: ''}; - this.history = new InputHistory(this.props.history); - } - - public componentDidUpdate(prevProps: Readonly, prevState: Readonly, snapshot?: {}) { - // Force our new value. the RCM control doesn't do this correctly - if (this.codeMirror && this.props.readOnly && this.codeMirror.getValue() !== this.props.code) { - this.codeMirror.setValue(this.props.code); - } - } - - public render() { - const readOnly = this.props.testMode || this.props.readOnly; - const classes = readOnly ? 'code-area' : 'code-area code-area-editable'; - return ( -
-
- ); - } - - public onParentClick(ev: React.MouseEvent) { - const readOnly = this.props.testMode || this.props.readOnly; - if (this.codeMirror && !readOnly) { - ev.stopPropagation(); - this.codeMirror.focus(); - } - } - - private onCursorActivity = (codeMirror: CodeMirror.Editor) => { - // Update left/top/char for cursor - if (codeMirror) { - const doc = codeMirror.getDoc(); - const selections = doc.listSelections(); - const cursor = doc.getCursor(); - const anchor = selections && selections.length > 0 ? selections[selections.length - 1].anchor : {ch: 10000, line: 10000}; - const wantStart = cursor.line < anchor.line || cursor.line === anchor.line && cursor.ch < anchor.ch; - const coords = codeMirror.cursorCoords(wantStart, 'local'); - const char = this.getCursorChar(); - this.setState({ - cursorLeft: coords.left, - cursorTop: coords.top, - cursorBottom: coords.bottom, - charUnderCursor: char - }); - } - - } - - private getCursorChar = () : string => { - if (this.codeMirror) { - const doc = this.codeMirror.getDoc(); - const cursorPos = doc.getCursor(); - const line = doc.getLine(cursorPos.line); - if (line.length > cursorPos.ch) { - return line.slice(cursorPos.ch, cursorPos.ch + 1); - } - } - - // We don't need a state update on cursor change because - // we only really need this on focus change - return ''; - } - - private onFocusChange = (focused: boolean) => { - this.setState({focused}); - } - - private updateCodeMirror = (rcm: ReactCodeMirror.ReactCodeMirror) => { - if (rcm) { - this.codeMirror = rcm.getCodeMirror(); - const coords = this.codeMirror.cursorCoords(false, 'local'); - const char = this.getCursorChar(); - this.setState({ - cursorLeft: coords.left, - cursorTop: coords.top, - cursorBottom: coords.bottom, - charUnderCursor: char - }); - } - } - - private getBaseIndentation(instance: CodeMirror.Editor) : number { - if (!this.baseIndentation) { - const option = instance.getOption('indentUnit'); - if (option) { - this.baseIndentation = parseInt(option.toString(), 10); - } else { - this.baseIndentation = 2; - } - } - return this.baseIndentation; - } - - private expectedIndent(instance: CodeMirror.Editor, line: number) : number { - // Expected should be indent on the previous line and one more if line - // ends with : - const doc = instance.getDoc(); - const baseIndent = this.getBaseIndentation(instance); - const lineStr = doc.getLine(line).trimRight(); - const lastChar = lineStr.length === 0 ? null : lineStr.charAt(lineStr.length - 1); - const frontIndent = lineStr.length - lineStr.trimLeft().length; - return frontIndent + (lastChar === ':' ? baseIndent : 0); - } - - private shiftEnter = (instance: CodeMirror.Editor) => { - // Shift enter is always submit (for now) - const doc = instance.getDoc(); - // Double check we don't have an entirely empty document - if (doc.getValue('').trim().length > 0) { - let code = doc.getValue(); - // We have to clear the history as this CodeMirror doesn't go away. - doc.clearHistory(); - doc.setValue(''); - - // Submit without the last extra line if we have one. - if (code.endsWith('\n\n')) { - code = code.slice(0, code.length - 1); - } - - this.props.onSubmit(code); - return; - } - } - - private enter = (instance: CodeMirror.Editor) => { - // See if the cursor is at the end of a single line or if on an indented line. Any indent - // or line ends with : or ;\, then don't submit - const doc = instance.getDoc(); - const cursor = doc.getCursor(); - const lastLine = doc.lastLine(); - if (cursor.line === lastLine) { - - // Check for any text - const line = doc.getLine(lastLine); - if (line.length === 0) { - // Do the same thing as shift+enter - this.shiftEnter(instance); - return; - } - } - - // Otherwise add a line and indent the appropriate amount - const expectedIndents = this.expectedIndent(instance, cursor.line); - const indentString = Array(expectedIndents + 1).join(' '); - doc.replaceRange(`\n${indentString}`, { line: cursor.line, ch: doc.getLine(cursor.line).length }); - doc.setCursor({line: cursor.line + 1, ch: indentString.length}); - - // Tell our listener we added a new line - this.props.onChangeLineCount(doc.lineCount()); - } - - private arrowUp = (instance: CodeMirror.Editor) => { - if (instance.getDoc().getCursor().line === 0 && instance.getDoc().getCursor().ch === 0) { - instance.getDoc().setValue(this.history.completeUp()); - return; - } - return CodeMirror.Pass; - } - - private arrowDown = (instance: CodeMirror.Editor) => { - if (instance.getDoc().getCursor().line === 0 && instance.getDoc().getCursor().ch === 0) { - instance.getDoc().setValue(this.history.completeDown()); - return; - } - return CodeMirror.Pass; - } - - private onChange = (newValue: string, change: CodeMirror.EditorChange) => { - this.history.onChange(); - } -} +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import 'codemirror/lib/codemirror.css'; +import 'codemirror/mode/python/python'; + +import * as CodeMirror from 'codemirror'; +import * as React from 'react'; +import * as RCM from 'react-codemirror'; + +import './code.css'; + +import { getLocString } from '../react-common/locReactSide'; +import { Cursor } from './cursor'; +import { InputHistory } from './inputHistory'; + +export interface ICodeProps { + autoFocus: boolean; + code : string; + codeTheme: string; + testMode: boolean; + readOnly: boolean; + history: string[]; + cursorType: string; + showWatermark: boolean; + onSubmit(code: string): void; + onChangeLineCount(lineCount: number) : void; + +} + +interface ICodeState { + focused: boolean; + cursorLeft: number; + cursorTop: number; + cursorBottom: number; + charUnderCursor: string; + allowWatermark: boolean; +} + +export class Code extends React.Component { + + private codeMirror: CodeMirror.Editor | undefined; + private history : InputHistory; + private baseIndentation : number | undefined; + + constructor(prop: ICodeProps) { + super(prop); + this.state = {focused: false, cursorLeft: 0, cursorTop: 0, cursorBottom: 0, charUnderCursor: '', allowWatermark: true}; + this.history = new InputHistory(this.props.history); + } + + public componentDidUpdate(prevProps: Readonly, prevState: Readonly, snapshot?: {}) { + // Force our new value. the RCM control doesn't do this correctly + if (this.codeMirror && this.props.readOnly && this.codeMirror.getValue() !== this.props.code) { + this.codeMirror.setValue(this.props.code); + } + } + + public render() { + const readOnly = this.props.testMode || this.props.readOnly; + const classes = readOnly ? 'code-area' : 'code-area code-area-editable'; + const waterMarkClass = this.props.showWatermark && this.state.allowWatermark && !readOnly ? 'code-watermark' : 'hide'; + return ( +
+
+ ); + } + + public onParentClick(ev: React.MouseEvent) { + const readOnly = this.props.testMode || this.props.readOnly; + if (this.codeMirror && !readOnly) { + ev.stopPropagation(); + this.codeMirror.focus(); + } + } + + private getWatermarkString = () : string => { + return getLocString('DataScience.inputWatermark', 'Shift-enter to run'); + } + + private onCursorActivity = (codeMirror: CodeMirror.Editor) => { + // Update left/top/char for cursor + if (codeMirror) { + const doc = codeMirror.getDoc(); + const selections = doc.listSelections(); + const cursor = doc.getCursor(); + const anchor = selections && selections.length > 0 ? selections[selections.length - 1].anchor : {ch: 10000, line: 10000}; + const wantStart = cursor.line < anchor.line || cursor.line === anchor.line && cursor.ch < anchor.ch; + const coords = codeMirror.cursorCoords(wantStart, 'local'); + const char = this.getCursorChar(); + this.setState({ + cursorLeft: coords.left, + cursorTop: coords.top, + cursorBottom: coords.bottom, + charUnderCursor: char + }); + } + + } + + private getCursorChar = () : string => { + if (this.codeMirror) { + const doc = this.codeMirror.getDoc(); + const cursorPos = doc.getCursor(); + const line = doc.getLine(cursorPos.line); + if (line.length > cursorPos.ch) { + return line.slice(cursorPos.ch, cursorPos.ch + 1); + } + } + + // We don't need a state update on cursor change because + // we only really need this on focus change + return ''; + } + + private onFocusChange = (focused: boolean) => { + this.setState({focused}); + } + + private updateCodeMirror = (rcm: ReactCodeMirror.ReactCodeMirror) => { + if (rcm) { + this.codeMirror = rcm.getCodeMirror(); + const coords = this.codeMirror.cursorCoords(false, 'local'); + const char = this.getCursorChar(); + this.setState({ + cursorLeft: coords.left, + cursorTop: coords.top, + cursorBottom: coords.bottom, + charUnderCursor: char + }); + } + } + + private getBaseIndentation(instance: CodeMirror.Editor) : number { + if (!this.baseIndentation) { + const option = instance.getOption('indentUnit'); + if (option) { + this.baseIndentation = parseInt(option.toString(), 10); + } else { + this.baseIndentation = 2; + } + } + return this.baseIndentation; + } + + private expectedIndent(instance: CodeMirror.Editor, line: number) : number { + // Expected should be indent on the previous line and one more if line + // ends with : + const doc = instance.getDoc(); + const baseIndent = this.getBaseIndentation(instance); + const lineStr = doc.getLine(line).trimRight(); + const lastChar = lineStr.length === 0 ? null : lineStr.charAt(lineStr.length - 1); + const frontIndent = lineStr.length - lineStr.trimLeft().length; + return frontIndent + (lastChar === ':' ? baseIndent : 0); + } + + private shiftEnter = (instance: CodeMirror.Editor) => { + // Shift enter is always submit (for now) + const doc = instance.getDoc(); + // Double check we don't have an entirely empty document + if (doc.getValue('').trim().length > 0) { + let code = doc.getValue(); + // We have to clear the history as this CodeMirror doesn't go away. + doc.clearHistory(); + doc.setValue(''); + + // Submit without the last extra line if we have one. + if (code.endsWith('\n\n')) { + code = code.slice(0, code.length - 1); + } + + this.props.onSubmit(code); + return; + } + } + + private enter = (instance: CodeMirror.Editor) => { + // See if the cursor is at the end of a single line or if on an indented line. Any indent + // or line ends with : or ;\, then don't submit + const doc = instance.getDoc(); + const cursor = doc.getCursor(); + const lastLine = doc.lastLine(); + if (cursor.line === lastLine) { + + // Check for any text + const line = doc.getLine(lastLine); + if (line.length === 0) { + // Do the same thing as shift+enter + this.shiftEnter(instance); + return; + } + } + + // Otherwise add a line and indent the appropriate amount + const expectedIndents = this.expectedIndent(instance, cursor.line); + const indentString = Array(expectedIndents + 1).join(' '); + doc.replaceRange(`\n${indentString}`, { line: cursor.line, ch: doc.getLine(cursor.line).length }); + doc.setCursor({line: cursor.line + 1, ch: indentString.length}); + + // Tell our listener we added a new line + this.props.onChangeLineCount(doc.lineCount()); + } + + private arrowUp = (instance: CodeMirror.Editor) => { + if (instance.getDoc().getCursor().line === 0 && instance.getDoc().getCursor().ch === 0) { + instance.getDoc().setValue(this.history.completeUp()); + return; + } + return CodeMirror.Pass; + } + + private arrowDown = (instance: CodeMirror.Editor) => { + if (instance.getDoc().getCursor().line === 0 && instance.getDoc().getCursor().ch === 0) { + instance.getDoc().setValue(this.history.completeDown()); + return; + } + return CodeMirror.Pass; + } + + private onChange = (newValue: string, change: CodeMirror.EditorChange) => { + this.history.onChange(); + this.setState({allowWatermark: false}); + } +} diff --git a/src/datascience-ui/history-react/mainPanelState.ts b/src/datascience-ui/history-react/mainPanelState.ts index afb9f5418a7a..e7caf7fcc048 100644 --- a/src/datascience-ui/history-react/mainPanelState.ts +++ b/src/datascience-ui/history-react/mainPanelState.ts @@ -18,6 +18,7 @@ export interface IMainPanelState { undoStack : ICellViewModel[][]; redoStack : ICellViewModel[][]; historyStack: string[]; + submittedText: boolean; } // This function generates test state when running under a browser instead of inside of @@ -28,7 +29,8 @@ export function generateTestState(inputBlockToggled : (id: string) => void, file skipNextScroll : false, undoStack : [], redoStack : [], - historyStack: [] + historyStack: [], + submittedText: false }; }