2018-11-02 06:26:24 +01:00
/ * * # C o m m a n d l i n e f u n c t i o n s
*
* This file contains functions to interact with the command line .
*
* If you want to bind them to keyboard shortcuts , be sure to prefix them with "ex." . For example , if you want to bind control - p to ` prev_completion ` , use :
*
* ` ` `
* bind -- mode = ex < C - p > ex . prev_completion
* ` ` `
*
2018-11-08 07:00:35 +01:00
* Note that you can also bind Tridactyl ' s [ editor functions ] ( / s t a t i c / d o c s / m o d u l e s / _ s r c _ l i b _ e d i t o r _ . h t m l ) i n t h e c o m m a n d l i n e .
2018-11-02 06:26:24 +01:00
*
* Contrary to the main tridactyl help page , this one doesn 't tell you whether a specific function is bound to something. For now, you' ll have to make do with ` :bind ` and ` :viewconfig ` .
*
* /
/** ignore this line */
2017-09-29 18:29:36 +01:00
/** Script used in the commandline iframe. Communicates with background. */
2017-10-02 00:59:51 +01:00
2018-10-04 13:59:19 +01:00
import * as perf from "@src/perf"
import "@src/lib/number.clamp"
2018-09-29 17:38:58 -07:00
import "@src/lib/html-tagged-template"
import * as Completions from "@src/completions"
2018-11-19 07:14:08 +01:00
import { TabAllCompletionSource } from "@src/completions/TabAll"
2018-11-20 20:16:07 +00:00
import { BufferCompletionSource } from "@src/completions/Tab"
2018-09-29 17:38:58 -07:00
import { BmarkCompletionSource } from "@src/completions/Bmark"
import { ExcmdCompletionSource } from "@src/completions/Excmd"
2018-10-11 16:55:01 +02:00
import { FileSystemCompletionSource } from "@src/completions/FileSystem"
2018-10-10 11:31:27 +02:00
import { HelpCompletionSource } from "@src/completions/Help"
2018-09-29 17:38:58 -07:00
import { HistoryCompletionSource } from "@src/completions/History"
2018-10-12 12:44:14 +02:00
import { PreferenceCompletionSource } from "@src/completions/Preferences"
2018-09-29 17:38:58 -07:00
import { SettingsCompletionSource } from "@src/completions/Settings"
2018-09-29 16:17:52 -07:00
import * as Messaging from "@src/lib/messaging"
2018-09-29 15:57:09 -07:00
import * as Config from "@src/lib/config"
2018-09-29 16:23:06 -07:00
import "@src/lib/number.clamp"
2018-09-29 17:38:58 -07:00
import state from "@src/state"
2018-09-29 15:30:48 -07:00
import Logger from "@src/lib/logging"
2018-09-29 16:43:18 -07:00
import { theme } from "@src/content/styling"
2018-10-04 13:59:19 +01:00
2018-10-31 08:09:51 +01:00
import * as genericParser from "@src/parsers/genericmode"
import * as tri_editor from "@src/lib/editor"
2018-11-02 06:26:24 +01:00
/** @hidden **/
2018-04-13 19:28:03 +01:00
const logger = new Logger ( "cmdline" )
2017-10-05 23:11:56 +01:00
2018-11-02 06:26:24 +01:00
/** @hidden **/
2017-11-22 18:05:54 +00:00
let activeCompletions : Completions.CompletionSource [ ] = undefined
2018-11-02 06:26:24 +01:00
/** @hidden **/
2018-04-13 19:28:03 +01:00
let completionsDiv = window . document . getElementById (
"completions" ,
) as HTMLElement
2018-11-02 06:26:24 +01:00
/** @hidden **/
2018-04-13 19:28:03 +01:00
let clInput = window . document . getElementById (
"tridactyl-input" ,
) as HTMLInputElement
2017-10-12 04:02:01 +01:00
2018-05-20 13:17:28 +01:00
// first theming of commandline iframe
theme ( document . querySelector ( ":root" ) )
2018-11-02 06:26:24 +01:00
/ * * @ h i d d e n
* This is to handle Escape key which , while the cmdline is focused ,
2017-11-15 13:41:04 -08:00
* ends up firing both keydown and input listeners . In the worst case
* hides the cmdline , shows and refocuses it and replaces its text
* which could be the prefix to generate a completion .
* tl ; dr TODO : delete this and better resolve race condition
* /
let isVisible = false
2018-11-02 06:26:24 +01:00
/** @hidden **/
2017-11-22 18:05:54 +00:00
function resizeArea() {
if ( isVisible ) {
2018-10-02 12:44:35 +02:00
Messaging . messageOwnTab ( "commandline_content" , "show" )
Messaging . messageOwnTab ( "commandline_content" , "focus" )
focus ( )
2017-11-22 18:05:54 +00:00
}
}
2018-11-02 06:26:24 +01:00
/ * * @ h i d d e n
* This is a bit loosely defined at the moment .
* Should work so long as there ' s only one completion source per prefix .
* /
2017-11-22 18:13:31 +00:00
function getCompletion() {
2018-10-02 16:40:38 +02:00
if ( ! activeCompletions ) return undefined
2017-11-22 18:13:31 +00:00
for ( const comp of activeCompletions ) {
2018-04-13 19:28:03 +01:00
if ( comp . state === "normal" && comp . completion !== undefined ) {
2017-11-22 18:13:31 +00:00
return comp . completion
}
}
}
2018-11-02 06:26:24 +01:00
/** @hidden **/
2018-10-31 08:09:51 +01:00
export function enableCompletions() {
2018-04-13 19:28:03 +01:00
if ( ! activeCompletions ) {
2017-11-22 18:05:54 +00:00
activeCompletions = [
2018-08-05 16:34:36 +02:00
new BmarkCompletionSource ( completionsDiv ) ,
2018-11-19 07:14:08 +01:00
new TabAllCompletionSource ( completionsDiv ) ,
2018-11-20 20:16:07 +00:00
new BufferCompletionSource ( completionsDiv ) ,
2018-08-05 16:34:36 +02:00
new ExcmdCompletionSource ( completionsDiv ) ,
2018-10-11 16:55:01 +02:00
new FileSystemCompletionSource ( completionsDiv ) ,
2018-10-10 11:31:27 +02:00
new HelpCompletionSource ( completionsDiv ) ,
2018-06-19 07:59:39 +02:00
new HistoryCompletionSource ( completionsDiv ) ,
2018-10-12 12:44:14 +02:00
new PreferenceCompletionSource ( completionsDiv ) ,
2018-10-10 11:31:27 +02:00
new SettingsCompletionSource ( completionsDiv ) ,
2017-11-22 18:05:54 +00:00
]
2017-11-15 13:41:04 -08:00
2017-11-22 18:13:31 +00:00
const fragment = document . createDocumentFragment ( )
activeCompletions . forEach ( comp = > fragment . appendChild ( comp . node ) )
completionsDiv . appendChild ( fragment )
2018-10-02 16:40:38 +02:00
logger . debug ( activeCompletions )
2017-11-22 18:05:54 +00:00
}
}
2017-11-22 18:13:31 +00:00
/* document.addEventListener("DOMContentLoaded", enableCompletions) */
2018-11-02 06:26:24 +01:00
/** @hidden **/
2018-04-13 19:28:03 +01:00
let noblur = e = > setTimeout ( ( ) = > clInput . focus ( ) , 0 )
2017-11-25 23:15:45 +00:00
2018-11-02 06:26:24 +01:00
/** @hidden **/
2017-11-22 18:13:31 +00:00
export function focus() {
clInput . focus ( )
2018-04-13 19:28:03 +01:00
clInput . addEventListener ( "blur" , noblur )
2017-11-22 18:13:31 +00:00
}
2017-09-29 18:29:36 +01:00
2018-11-02 06:26:24 +01:00
/** @hidden **/
2017-10-28 05:11:10 +01:00
async function sendExstr ( exstr ) {
Messaging . message ( "commandline_background" , "recvExStr" , [ exstr ] )
}
2018-11-02 06:26:24 +01:00
/** @hidden **/
2017-11-26 14:06:13 +00:00
let HISTORY_SEARCH_STRING : string
2018-11-02 06:26:24 +01:00
/ * * @ h i d d e n
* Command line keybindings
* * /
2018-10-31 08:09:51 +01:00
let keyParser = keys = > genericParser . parser ( "exmaps" , keys )
2018-11-02 06:26:24 +01:00
/** @hidden **/
2018-10-31 08:09:51 +01:00
let keyEvents = [ ]
2018-11-02 06:26:24 +01:00
/** @hidden **/
2018-11-04 12:28:31 +00:00
clInput . addEventListener (
"keydown" ,
function ( keyevent : KeyboardEvent ) {
keyEvents . push ( keyevent )
let response = keyParser ( keyEvents )
if ( response . isMatch ) {
2017-11-22 23:01:34 +00:00
keyevent . preventDefault ( )
2018-11-04 12:28:31 +00:00
keyevent . stopImmediatePropagation ( )
}
if ( response . exstr ) {
keyEvents = [ ]
Messaging . message ( "controller_background" , "acceptExCmd" , [
response . exstr ,
] )
} else {
keyEvents = response . keys
}
} ,
true ,
)
2017-11-26 14:06:13 +00:00
2018-11-02 06:26:24 +01:00
/ * *
* Insert the first command line history line that starts with the content of the command line in the command line .
* /
export function complete() {
let fragment = clInput . value
let matches = state . cmdHistory . filter ( key = > key . startsWith ( fragment ) )
let mostrecent = matches [ matches . length - 1 ]
if ( mostrecent != undefined ) clInput . value = mostrecent
2018-11-06 02:19:34 +01:00
clInput . dispatchEvent ( new Event ( "input" ) ) // dirty hack for completions
2018-11-02 06:26:24 +01:00
}
/ * *
* Selects the next completion .
* /
2018-10-31 08:09:51 +01:00
export function next_completion() {
2018-11-04 12:28:31 +00:00
if ( activeCompletions ) activeCompletions . forEach ( comp = > comp . next ( ) )
2018-10-31 08:09:51 +01:00
}
2017-09-29 18:29:36 +01:00
2018-11-02 06:26:24 +01:00
/ * *
* Selects the previous completion .
* /
2018-11-01 19:11:57 +01:00
export function prev_completion() {
2018-11-04 12:28:31 +00:00
if ( activeCompletions ) activeCompletions . forEach ( comp = > comp . prev ( ) )
2018-10-31 08:09:51 +01:00
}
2018-11-02 06:26:24 +01:00
/ * *
* Inserts the currently selected completion and a space in the command line .
* /
2018-10-31 08:09:51 +01:00
export function insert_completion() {
const command = getCompletion ( )
2018-11-13 18:35:11 +01:00
if ( activeCompletions ) {
activeCompletions . forEach ( comp = > ( comp . completion = undefined ) )
}
2018-11-06 02:19:34 +01:00
if ( command ) {
clInput . value = command + " "
clInput . dispatchEvent ( new Event ( "input" ) ) // dirty hack for completions
}
}
/ * *
* If a completion is selected , inserts it in the command line with a space .
* If no completion is selected , inserts a space where the caret is .
* /
export function insert_space_or_completion() {
const command = getCompletion ( )
2018-11-13 18:35:11 +01:00
if ( activeCompletions ) {
activeCompletions . forEach ( comp = > ( comp . completion = undefined ) )
}
2018-11-06 02:19:34 +01:00
if ( command ) {
clInput . value = command + " "
} else {
const selectionStart = clInput . selectionStart
const selectionEnd = clInput . selectionEnd
2018-11-08 07:00:35 +01:00
clInput . value =
clInput . value . substring ( 0 , selectionStart ) +
" " +
clInput . value . substring ( selectionEnd )
2018-11-06 02:19:34 +01:00
clInput . selectionStart = clInput . selectionEnd = selectionStart + 1
}
2018-11-01 18:43:59 +01:00
clInput . dispatchEvent ( new Event ( "input" ) ) // dirty hack for completions
2018-10-31 08:09:51 +01:00
}
2017-09-29 18:29:36 +01:00
2018-11-02 06:26:24 +01:00
/** @hidden **/
2018-10-24 21:27:56 +02:00
let timeoutId : any = 0
2018-11-02 06:26:24 +01:00
/** @hidden **/
2018-10-11 16:55:01 +02:00
let onInputPromise : Promise < any > = Promise . resolve ( )
2018-11-02 06:26:24 +01:00
/** @hidden **/
2018-10-24 21:27:56 +02:00
clInput . addEventListener ( "input" , ( ) = > {
2017-12-31 16:00:00 +08:00
const exstr = clInput . value
2018-10-24 21:27:56 +02:00
// Prevent starting previous completion computation if possible
clearTimeout ( timeoutId )
// Schedule completion computation. We do not start computing immediately because this would incur a slow down on quickly repeated input events (e.g. maintaining <Backspace> pressed)
let myTimeoutId = setTimeout ( async ( ) = > {
try {
// Make sure the previous computation has ended
await onInputPromise
} catch ( e ) {
// we don't actually care because this is the previous computation, which we will throw away
logger . warning ( e )
}
2018-10-02 16:40:38 +02:00
2018-10-24 21:27:56 +02:00
// If we're not the current completion computation anymore, stop
if ( timeoutId != myTimeoutId ) return
enableCompletions ( )
// Fire each completion and add a callback to resize area
onInputPromise = Promise . all (
2018-11-04 12:28:31 +00:00
activeCompletions . map ( comp = > comp . filter ( exstr ) . then ( resizeArea ) ) ,
2018-10-24 21:27:56 +02:00
)
} , 100 )
// Declare self as current completion computation
timeoutId = myTimeoutId
2017-11-15 13:41:04 -08:00
} )
2018-11-02 06:26:24 +01:00
/** @hidden **/
2017-11-05 14:48:22 +00:00
let cmdline_history_position = 0
2018-11-02 06:26:24 +01:00
/** @hidden **/
2017-11-05 14:48:22 +00:00
let cmdline_history_current = ""
2018-11-02 06:26:24 +01:00
/ * * @ h i d d e n
* Clears the command line .
* If you intend to close the command line after this , set evlistener to true in order to enable losing focus .
2018-07-05 19:50:32 +02:00
* Otherwise , no need to pass an argument .
* /
export function clear ( evlistener = false ) {
if ( evlistener ) clInput . removeEventListener ( "blur" , noblur )
2017-11-22 23:01:34 +00:00
clInput . value = ""
2017-11-26 14:06:13 +00:00
cmdline_history_position = 0
cmdline_history_current = ""
2018-07-05 19:50:32 +02:00
}
2018-11-02 06:26:24 +01:00
/** Hide the command line and clear its content without executing it. **/
2018-07-05 19:50:32 +02:00
export async function hide_and_clear() {
clear ( true )
2018-11-01 19:11:57 +01:00
keyEvents = [ ]
2017-11-22 18:13:31 +00:00
// Try to make the close cmdline animation as smooth as possible.
2018-10-02 12:44:35 +02:00
Messaging . messageOwnTab ( "commandline_content" , "hide" )
Messaging . messageOwnTab ( "commandline_content" , "blur" )
2017-11-22 18:05:54 +00:00
// Delete all completion sources - I don't think this is required, but this
// way if there is a transient bug in completions it shouldn't persist.
2018-06-30 16:59:10 +02:00
if ( activeCompletions )
activeCompletions . forEach ( comp = > completionsDiv . removeChild ( comp . node ) )
2017-11-22 18:05:54 +00:00
activeCompletions = undefined
2017-11-15 13:41:04 -08:00
isVisible = false
2017-11-05 14:48:22 +00:00
}
2018-11-02 06:26:24 +01:00
/** @hidden **/
2018-01-05 21:36:12 -08:00
function setCursor ( n = 0 ) {
clInput . setSelectionRange ( n , n , "none" )
}
2018-11-02 06:26:24 +01:00
/ * *
* Selects the next history line .
* /
export function next_history() {
return history ( 1 )
2017-11-09 12:44:57 +00:00
}
2018-11-02 06:26:24 +01:00
/ * *
* Selects the prev history line .
* /
export function prev_history() {
return history ( - 1 )
}
/** @hidden **/
2018-04-13 19:28:03 +01:00
function history ( n ) {
HISTORY_SEARCH_STRING =
HISTORY_SEARCH_STRING === undefined
? clInput . value
: HISTORY_SEARCH_STRING
let matches = state . cmdHistory . filter ( key = >
key . startsWith ( HISTORY_SEARCH_STRING ) ,
)
if ( cmdline_history_position == 0 ) {
2017-11-15 13:41:04 -08:00
cmdline_history_current = clInput . value
2017-11-05 14:48:22 +00:00
}
2017-11-26 14:06:13 +00:00
let clamped_ind = matches . length + n - cmdline_history_position
clamped_ind = clamped_ind . clamp ( 0 , matches . length )
2017-11-09 08:04:05 +00:00
2017-11-26 14:06:13 +00:00
const pot_history = matches [ clamped_ind ]
2018-04-13 19:28:03 +01:00
clInput . value =
pot_history == undefined ? cmdline_history_current : pot_history
2017-11-26 14:06:13 +00:00
// if there was no clampage, update history position
// there's a more sensible way of doing this but that would require more programmer time
2018-04-13 19:28:03 +01:00
if ( clamped_ind == matches . length + n - cmdline_history_position )
cmdline_history_position = cmdline_history_position - n
2017-11-05 14:48:22 +00:00
}
2018-11-02 06:26:24 +01:00
/ * *
* Execute the content of the command line and hide it .
* * /
export function accept_line() {
2017-12-01 12:12:56 +00:00
const command = getCompletion ( ) || clInput . value
hide_and_clear ( )
2017-11-20 01:31:47 +00:00
2018-04-13 19:28:03 +01:00
const [ func , . . . args ] = command . trim ( ) . split ( /\s+/ )
2018-04-25 22:30:57 +01:00
2018-04-27 16:45:50 +01:00
if ( func . length === 0 || func . startsWith ( "#" ) ) {
2018-04-25 22:30:57 +01:00
return
}
// Save non-secret commandlines to the history.
2018-04-13 19:28:03 +01:00
if (
! browser . extension . inIncognitoContext &&
! ( func === "winopen" && args [ 0 ] === "-private" )
2017-11-20 01:31:47 +00:00
) {
2017-12-01 12:12:56 +00:00
state . cmdHistory = state . cmdHistory . concat ( [ command ] )
2017-11-09 08:19:28 +00:00
}
2017-11-05 14:48:22 +00:00
cmdline_history_position = 0
2017-11-22 18:05:54 +00:00
2017-12-01 12:12:56 +00:00
sendExstr ( command )
2017-09-29 18:29:36 +01:00
}
2017-10-02 00:59:51 +01:00
2018-11-02 06:26:24 +01:00
/** @hidden **/
2018-06-30 16:59:10 +02:00
export function fillcmdline (
newcommand? : string ,
trailspace = true ,
ffocus = true ,
) {
2018-07-01 17:30:10 +02:00
if ( trailspace ) clInput . value = newcommand + " "
else clInput . value = newcommand
2017-11-15 13:41:04 -08:00
isVisible = true
2018-06-30 16:59:10 +02:00
// Focus is lost for some reason.
if ( ffocus ) {
focus ( )
clInput . dispatchEvent ( new Event ( "input" ) ) // dirty hack for completions
}
2017-10-05 23:11:56 +01:00
}
2018-11-02 06:26:24 +01:00
/ * * @ h i d d e n
* Create a temporary textarea and give it to fn . Remove the textarea afterwards
*
* Useful for document . execCommand
* * /
2017-10-28 13:42:54 +01:00
function applyWithTmpTextArea ( fn ) {
let textarea
try {
textarea = document . createElement ( "textarea" )
// Scratchpad must be `display`ed, but can be tiny and invisible.
// Being tiny and invisible means it won't make the parent page move.
2018-04-13 19:28:03 +01:00
textarea . style . cssText =
"visible: invisible; width: 0; height: 0; position: fixed"
2017-10-28 13:42:54 +01:00
textarea . contentEditable = "true"
document . documentElement . appendChild ( textarea )
return fn ( textarea )
} finally {
document . documentElement . removeChild ( textarea )
}
2017-10-28 19:20:31 +08:00
}
2018-11-02 06:26:24 +01:00
/** @hidden **/
2018-07-26 10:17:46 +01:00
export async function setClipboard ( content : string ) {
2018-11-07 06:13:43 +01:00
await Messaging . messageOwnTab ( "commandline_content" , "focus" )
2018-02-19 00:15:44 +00:00
applyWithTmpTextArea ( scratchpad = > {
2017-10-28 19:20:31 +08:00
scratchpad . value = content
scratchpad . select ( )
2017-10-28 13:42:54 +01:00
if ( document . execCommand ( "Copy" ) ) {
// // todo: Maybe we can consider to using some logger and show it with status bar in the future
2018-04-13 19:28:03 +01:00
logger . info ( "set clipboard:" , scratchpad . value )
2017-10-28 13:42:54 +01:00
} else throw "Failed to copy!"
2017-10-28 19:20:31 +08:00
} )
2018-02-19 00:15:44 +00:00
// Return focus to the document
2018-11-07 06:13:43 +01:00
await Messaging . messageOwnTab ( "commandline_content" , "hide" )
return Messaging . messageOwnTab ( "commandline_content" , "blur" )
2017-10-28 19:20:31 +08:00
}
2018-11-02 06:26:24 +01:00
/** @hidden **/
2018-11-07 06:13:43 +01:00
export async function getClipboard() {
await Messaging . messageOwnTab ( "commandline_content" , "focus" )
2018-02-19 00:15:44 +00:00
const result = applyWithTmpTextArea ( scratchpad = > {
2017-10-28 19:20:31 +08:00
scratchpad . focus ( )
document . execCommand ( "Paste" )
2017-10-28 13:42:54 +01:00
return scratchpad . textContent
2017-10-28 19:20:31 +08:00
} )
2018-02-19 00:15:44 +00:00
// Return focus to the document
2018-11-07 06:13:43 +01:00
await Messaging . messageOwnTab ( "commandline_content" , "hide" )
await Messaging . messageOwnTab ( "commandline_content" , "blur" )
2018-02-19 00:15:44 +00:00
return result
2017-10-28 19:20:31 +08:00
}
2018-11-02 06:26:24 +01:00
/** @hidden **/
2018-07-01 18:17:35 +02:00
export function getContent() {
return clInput . value
}
2018-11-02 06:26:24 +01:00
/** @hidden **/
2018-12-04 07:12:26 +01:00
export function editor_function ( fn_name , . . . args ) {
2018-10-31 08:09:51 +01:00
if ( tri_editor [ fn_name ] ) {
2018-12-04 07:12:26 +01:00
tri_editor [ fn_name ] ( clInput , . . . args )
2018-11-06 02:19:34 +01:00
clInput . dispatchEvent ( new Event ( "input" ) ) // dirty hack for completions
2018-10-31 08:09:51 +01:00
} else {
// The user is using the command line so we can't log message there
// logger.error(`No editor function named ${fn_name}!`)
console . error ( ` No editor function named ${ fn_name } ! ` )
}
}
import * as SELF from "@src/commandline_frame"
2018-04-13 19:28:03 +01:00
Messaging . addListener ( "commandline_frame" , Messaging . attributeCaller ( SELF ) )
2018-09-03 03:13:42 -07:00
2018-09-05 00:13:25 -07:00
// Listen for statistics from the commandline iframe and send them to
// the background for collection. Attach the observer to the window
// object since there's apparently a bug that causes performance
// observers to be GC'd even if they're still the target of a
// callback.
2018-10-10 16:21:33 +02:00
; ( window as any ) . tri = Object . assign ( window . tri || { } , {
2018-09-05 00:13:25 -07:00
perfObserver : perf.listenForCounters ( ) ,
2018-09-03 03:13:42 -07:00
} )