diff --git a/keymaps/ink.cson b/keymaps/ink.cson index b874bd22..27006646 100644 --- a/keymaps/ink.cson +++ b/keymaps/ink.cson @@ -49,7 +49,14 @@ '.platform-win32 ink-terminal, .platform-linux ink-terminal': 'ctrl-shift-c': 'ink-terminal:copy' 'ctrl-shift-v': 'ink-terminal:paste' + 'ctrl-shift-f': 'ink-terminal:show-search' '.platform-darwin ink-terminal': 'cmd-c': 'ink-terminal:copy' 'cmd-v': 'ink-terminal:paste' + 'cmd-f': 'ink-terminal:show-search' + +'ink-terminal .search .searchinput atom-text-editor': + 'enter': 'ink-terminal:find-next' + 'shift-enter': 'ink-terminal:find-previous' + 'escape': 'ink-terminal:close-search' diff --git a/lib/console2/console.js b/lib/console2/console.js index 0eee3a32..0e62f7f5 100644 --- a/lib/console2/console.js +++ b/lib/console2/console.js @@ -8,18 +8,21 @@ import { Terminal } from 'xterm' import * as fit from 'xterm/lib/addons/fit/fit' import * as webLinks from 'xterm/lib/addons/webLinks/webLinks' import * as winptyCompat from 'xterm/lib/addons/winptyCompat/winptyCompat' +import * as search from 'xterm/lib/addons/search/search' import TerminalElement from './view' import PaneItem from '../util/pane-item' import ResizeDetector from 'element-resize-detector' import { debounce, throttle } from 'underscore-plus' import { closest } from './helpers' import { openExternal } from 'shell' +import SearchUI from './searchui' let getTerminal = el => closest(el, 'ink-terminal').getModel() Terminal.applyAddon(fit) Terminal.applyAddon(webLinks) Terminal.applyAddon(winptyCompat) +Terminal.applyAddon(search) var subs @@ -36,10 +39,30 @@ export default class InkTerminal extends PaneItem { let term = getTerminal(target) if (term != undefined) { term.paste(process.platform != 'win32') + }}, + 'ink-terminal:show-search': ({target}) => { + let term = getTerminal(target) + if (term != undefined) { + term.searchui.show() + }}, + 'ink-terminal:find-next': ({target}) => { + let term = getTerminal(target) + if (term != undefined) { + term.searchui.find(true) + }}, + 'ink-terminal:find-previous': ({target}) => { + let term = getTerminal(target) + if (term != undefined) { + term.searchui.find(false) + }}, + 'ink-terminal:close-search': ({target}) => { + let term = getTerminal(target) + if (term != undefined) { + term.searchui.hide() }} })) - subs.add(atom.workspace.onDidStopChangingActivePaneItem((item) => { + subs.add(atom.workspace.onDidChangeActivePaneItem((item) => { if (item instanceof InkTerminal) { item.view.initialize(item) item.terminal.focus() @@ -92,6 +115,8 @@ export default class InkTerminal extends PaneItem { this.view = new TerminalElement + this.searchui = new SearchUI(this.terminal) + etch.initialize(this) etch.update(this).then(() => { this.view.initialize(this) @@ -215,7 +240,7 @@ export default class InkTerminal extends PaneItem { bracketed && this.ty.write('\x1b[201~') // disable bracketed paste mode } - show (view) { + show () { this.terminal.focus() } diff --git a/lib/console2/searchui.js b/lib/console2/searchui.js new file mode 100644 index 00000000..bef07d51 --- /dev/null +++ b/lib/console2/searchui.js @@ -0,0 +1,168 @@ +'use babel' +/** @jsx etch.dom */ + +import etch from 'etch' +import { Raw, Button, toView } from '../util/etch.js' +import { CompositeDisposable, Emitter, TextEditor } from 'atom' +import { Terminal } from 'xterm' +import * as fit from 'xterm/lib/addons/fit/fit' +import * as webLinks from 'xterm/lib/addons/webLinks/webLinks' +import * as winptyCompat from 'xterm/lib/addons/winptyCompat/winptyCompat' +import * as search from 'xterm/lib/addons/search/search' +import TerminalElement from './view' +import PaneItem from '../util/pane-item' +import ResizeDetector from 'element-resize-detector' +import { debounce, throttle } from 'underscore-plus' +import { closest } from './helpers' +import { openExternal } from 'shell' + +export default class TerminalSearchUI { + constructor (terminal) { + this.terminal = terminal + this.editor = new TextEditor( {mini: true, placeholderText: 'Find in Terminal'} ) + + this.useRegex = false + this.matchCase = false + this.wholeWord = false + + this.initialized = false + this.errorMessage = '' + + etch.initialize(this) + } + + update () {} + + toggleRegex () { + this.useRegex = !this.useRegex + this.refs.toggleRegex.element.classList.toggle('selected') + } + + toggleCase () { + this.matchCase = !this.matchCase + this.refs.toggleCase.element.classList.toggle('selected') + } + + toggleWhole () { + this.wholeWord = !this.wholeWord + this.refs.toggleWhole.element.classList.toggle('selected') + } + + toggleError (show) { + let el = this.refs.errorMessage + show ? el.classList.remove('hidden') : el.classList.add('hidden') + etch.update(this) + } + + find (next) { + + let text = this.editor.getText() + if (this.useRegex) { + let msg = null + try { + new RegExp(text) + } catch (err) { + msg = err.message + } + + if (msg !== null) { + this.errorMessage = msg + this.toggleError(true) + this.blinkRed() + return false + } + } + this.toggleError(false) + + let found + if (next) { + found = this.terminal.findNext(text, { + regex: this.useRegex, + wholeWord: this.wholeWord, + caseSensitive: this.matchCase, + incremental: false + }) + } else { + found = this.terminal.findPrevious(text, { + regex: this.useRegex, + wholeWord: this.wholeWord, + caseSensitive: this.matchCase + }) + } + + if (!found) this.blinkRed() + } + + blinkRed () { + this.element.classList.add('nothingfound') + setTimeout(() => this.element.classList.remove('nothingfound'), 200) + } + + render () { + return
+
+ { toView(this.editor.element) } +
+ + + +
+
+ + +
+
+ +
+
+
+ {this.errorMessage} +
+
+ } + + attach (element) { + if (!this.initialized) { + element.appendChild(this.element) + this.initialized = true + } + } + + show () { + this.element.classList.remove('hidden') + this.editor.element.focus() + } + + hide () { + this.element.classList.add('hidden') + this.terminal.focus() + } +} diff --git a/lib/console2/view.js b/lib/console2/view.js index fcffb3be..198b0426 100644 --- a/lib/console2/view.js +++ b/lib/console2/view.js @@ -48,6 +48,8 @@ class TerminalElement extends HTMLElement { this.themeTerminal() this.initMouseHandling() + this.model.searchui.attach(this) + return this } diff --git a/lib/util/etch.js b/lib/util/etch.js index 025e2066..dac37bfd 100644 --- a/lib/util/etch.js +++ b/lib/util/etch.js @@ -135,8 +135,9 @@ export class Badge extends Etch { export class Button extends Etch { render() { let iconclass = this.props.icon ? ` icon icon-${this.props.icon}` : ''; + let classname = this.props.className || '' return - ; diff --git a/package-lock.json b/package-lock.json index ca74e063..4abf20e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -677,9 +677,9 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "xterm": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/xterm/-/xterm-3.9.1.tgz", - "integrity": "sha512-5AZlhP0jvH/Sskx1UvvNFMqDRHVFqapl59rjV3RRpTJmveoharJplxPfzSThk85I4+AZo2xvD0X0nh0AAzkeZQ==" + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/xterm/-/xterm-3.10.0.tgz", + "integrity": "sha512-ZA55MObCk8xKHG0TjZpStCJn6oIkPkW24ZtagZpUdol9kHYJ36imPsesZdVXsyxQd1le4RPpSpDU9O/rqq/yCA==" }, "y18n": { "version": "3.2.1", diff --git a/package.json b/package.json index 53cd4c06..8ca9ec7f 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "katex": "^0.9.0", "fs-extra": "^5.0.0", "replace-in-file": "^3.0.0", - "xterm": "3.9.1", + "xterm": "3.10.0", "chroma-js": "^1.3.7" }, "scripts": { diff --git a/styles/terminal.less b/styles/terminal.less index 31ae94ff..edef489f 100644 --- a/styles/terminal.less +++ b/styles/terminal.less @@ -1,5 +1,5 @@ @import "syntax-variables"; - +@import "ui-variables"; ink-terminal, .ink-terminal { height: 100%; @@ -14,6 +14,43 @@ ink-terminal, .ink-terminal { background-color: @syntax-background-color !important; } } + + .search { + position: absolute; + top: 0.5em; + right: 1em; + background-color: @app-background-color; + width: 30em; + padding: 0.5em; + z-index: 5; + transition: border-color 0.2s; + border: 2px @base-border-color solid; + border-radius: 2px; + + .inputs { + flex-direction: row; + display: flex; + + >* { + padding-left: 0.5em; + margin: auto; + } + + .searchinput { + flex: 1; + padding-left: 0; + } + } + + .errormessage { + color: @text-color-error; + margin-top: 0.5em; + } + + &.nothingfound { + border-color: @background-color-error; + } + } } .scrollbars-visible-always {