1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-11-27 08:21:03 +02:00

Improved autocompletion handling

This commit is contained in:
Laurent Cozic 2017-08-04 18:02:43 +02:00
parent 9ed22265ba
commit 2868a28422
16 changed files with 207 additions and 168 deletions

View File

@ -14,7 +14,7 @@ import { _, setLocale, defaultLocale, closestSupportedLocale } from 'lib/locale.
import os from 'os';
import fs from 'fs-extra';
import yargParser from 'yargs-parser';
import { handleAutocompletion } from './autocompletion.js';
import { handleAutocompletion, installAutocompletionFile } from './autocompletion.js';
import { cliUtils } from './cli-utils.js';
class Application {
@ -157,6 +157,12 @@ class Application {
continue;
}
if (arg == '--ac-install') {
this.autocompletion_.install = true;
argv.splice(0, 1);
continue;
}
if (arg == '--ac-current') {
if (!nextArg) throw new Error(_('Usage: %s', '--ac-current <num>'));
this.autocompletion_.current = nextArg;
@ -309,6 +315,8 @@ class Application {
let initArgs = startFlags.matched;
if (argv.length) this.showPromptString_ = false;
Setting.setConstant('appName', initArgs.env == 'dev' ? 'joplindev' : 'joplin');
const profileDir = initArgs.profileDir ? initArgs.profileDir : os.homedir() + '/.config/' + Setting.value('appName');
const resourceDir = profileDir + '/resources';
const tempDir = profileDir + '/tmp';
@ -364,11 +372,26 @@ class Application {
Setting.setValue('activeFolderId', this.currentFolder_ ? this.currentFolder_.id : '');
if (this.autocompletion_.active) {
let items = await handleAutocompletion(this.autocompletion_);
for (let i = 0; i < items.length; i++) {
items[i] = items[i].replace(/ /g, '\\ ');
if (this.autocompletion_.install) {
try {
installAutocompletionFile(Setting.value('appName'), Setting.value('profileDir'));
} catch (error) {
if (error.code == 'shellNotSupported') {
console.info(error.message);
return;
}
throw error;
}
} else {
let items = await handleAutocompletion(this.autocompletion_);
if (!items.length) return;
for (let i = 0; i < items.length; i++) {
items[i] = items[i].replace(/ /g, '\\ ');
}
//console.info(items);
console.info(items.join("\n"));
}
console.info(items.join("\n"));
return;
}

View File

@ -1,14 +1,57 @@
import { app } from './app.js';
import { Note } from 'lib/models/note.js';
import { Folder } from 'lib/models/folder.js';
import { Tag } from 'lib/models/tag.js';
import { cliUtils } from './cli-utils.js';
import { _ } from 'lib/locale.js';
import fs from 'fs-extra';
import os from 'os';
import yargParser from 'yargs-parser';
function autocompletionFileContent(appName) {
let content = fs.readFileSync(__dirname + '/autocompletion_template.txt', 'utf8');
content = content.replace(/\|__APPNAME__\|/g, appName);
return content;
}
function installAutocompletionFile(appName, profileDir) {
if (process.env.SHELL.indexOf('bash') < 0) {
let error = new Error(_('Only Bash is currently supported for autocompletion.'));
error.code = 'shellNotSupported';
throw error;
}
const content = autocompletionFileContent(appName);
const filePath = profileDir + '/autocompletion.sh';
fs.writeFileSync(filePath, content);
const bashProfilePath = os.homedir() + '/.bashrc';
let bashrcContent = fs.readFileSync(bashProfilePath, 'utf8');
const lineToAdd = 'source ' + filePath;
console.info(_('Adding autocompletion script to: "%s"', bashProfilePath));
if (bashrcContent.indexOf(lineToAdd) >= 0) {
console.info(_('Autocompletion script is already installed.'));
} else {
bashrcContent += "\n" + lineToAdd + "\n";
fs.writeFileSync(bashProfilePath, bashrcContent);
console.info(_('Autocompletion has been installed.'));
}
console.info(_('Sourcing "%s"...', filePath));
const spawnSync = require('child_process').spawnSync;
spawnSync('source', [filePath]);
}
async function handleAutocompletion(autocompletion) {
let args = autocompletion.line.slice();
args.splice(0, 1);
let current = autocompletion.current - 1;
const currentWord = args[current];
const currentWord = args[current] ? args[current] : '';
// Auto-complete the command name
@ -32,21 +75,21 @@ async function handleAutocompletion(autocompletion) {
// Auto-complete the command options
if (!currentWord) return [];
if (currentWord) {
const includeLongs = currentWord.length == 1 ? currentWord.substr(0, 1) == '-' : currentWord.substr(0, 2) == '--';
const includeShorts = currentWord.length <= 2 && currentWord.substr(0, 1) == '-' && currentWord.substr(0, 2) != '--';
const includeLongs = currentWord.length == 1 ? currentWord.substr(0, 1) == '-' : currentWord.substr(0, 2) == '--';
const includeShorts = currentWord.length <= 2 && currentWord.substr(0, 1) == '-' && currentWord.substr(0, 2) != '--';
if (includeLongs || includeShorts) {
const output = [];
for (let i = 0; i < options.length; i++) {
const flags = cliUtils.parseFlags(options[i][0]);
const long = flags.long ? '--' + flags.long : null;
const short = flags.short ? '-' + flags.short : null;
if (includeLongs && long && long.indexOf(currentWord) === 0) output.push(long);
if (includeShorts && short && short.indexOf(currentWord) === 0) output.push(short);
if (includeLongs || includeShorts) {
const output = [];
for (let i = 0; i < options.length; i++) {
const flags = cliUtils.parseFlags(options[i][0]);
const long = flags.long ? '--' + flags.long : null;
const short = flags.short ? '-' + flags.short : null;
if (includeLongs && long && long.indexOf(currentWord) === 0) output.push(long);
if (includeShorts && short && short.indexOf(currentWord) === 0) output.push(short);
}
return output;
}
return output;
}
// Auto-complete the command arguments
@ -70,7 +113,7 @@ async function handleAutocompletion(autocompletion) {
let argName = cmdUsage[argIndex];
argName = cliUtils.parseCommandArg(argName).name;
if (argName == 'note') {
if (argName == 'note' || argName == 'note-pattern') {
if (!app().currentFolder()) return [];
const notes = await Note.previews(app().currentFolder().id, { titlePattern: currentWord + '*' });
return notes.map((n) => n.title);
@ -81,7 +124,29 @@ async function handleAutocompletion(autocompletion) {
return folders.map((n) => n.title);
}
if (argName == 'tag') {
let tags = await Tag.search({ titlePattern: currentWord + '*' });
return tags.map((n) => n.title);
}
if (argName == 'tag-command') {
return filterList(['add', 'remove', 'list'], currentWord);
}
if (argName == 'todo-command') {
return filterList(['toggle', 'clear'], currentWord);
}
return [];
}
export { handleAutocompletion };
function filterList(list, currentWord) {
let output = [];
for (let i = 0; i < list.length; i++) {
if (list[i].indexOf(currentWord) !== 0) continue;
output.push(list[i]);
}
return output;
}
export { handleAutocompletion, installAutocompletionFile };

View File

@ -0,0 +1,40 @@
export IFS=$'\n'
_|__APPNAME__|_completion() {
# COMP_WORDS contains each word in the current command, the last one
# being the one that needs to be completed. Convert this array
# to an "escaped" line which can be passed to the joplin CLI
# which will provide the possible autocompletion words.
ESCAPED_LINE=""
for WORD in "${COMP_WORDS[@]}"
do
if [[ -n $ESCAPED_LINE ]]; then
ESCAPED_LINE="$ESCAPED_LINE|__SEP__|"
fi
WORD="${WORD/\"/|__QUOTE__|}"
ESCAPED_LINE="$ESCAPED_LINE$WORD"
done
# Call joplin with the --autocompletion flag to retrieve the autocompletion
# candidates (each on its own line), and put these into COMREPLY.
# echo "joplindev --autocompletion --ac-current "$COMP_CWORD" --ac-line "$ESCAPED_LINE"" > ~/test.txt
COMPREPLY=()
while read -r line; do
COMPREPLY+=("$line")
done <<< "$(|__APPNAME__| --autocompletion --ac-current "$COMP_CWORD" --ac-line "$ESCAPED_LINE")"
# If there's only one element and it's empty, make COMREPLY
# completely empty so that default completion takes over
# (i.e. regular file completion)
# https://stackoverflow.com/a/19062943/561309
if [[ -z ${COMPREPLY[0]} ]]; then
COMPREPLY=()
fi
}
complete -o default -F _|__APPNAME__|_completion |__APPNAME__|

View File

@ -1,32 +0,0 @@
import { BaseCommand } from './base-command.js';
import { app } from './app.js';
import { _ } from 'lib/locale.js';
import { Setting } from 'lib/models/setting.js';
class Command extends BaseCommand {
usage() {
return 'alias <name> <command>';
}
description() {
return 'Creates a new command alias which can then be used as a regular command (eg. `alias ll "ls -l"`)';
}
async action(args) {
let aliases = Setting.value('aliases').trim();
aliases = aliases.length ? JSON.parse(aliases) : [];
aliases.push({
name: args.name,
command: args.command,
});
Setting.setValue('aliases', JSON.stringify(aliases));
}
enabled() {
return false; // Doesn't work properly at the moment
}
}
module.exports = Command;

View File

@ -1,54 +0,0 @@
import { BaseCommand } from './base-command.js';
import { app } from './app.js';
import { _ } from 'lib/locale.js';
import { Note } from 'lib/models/note.js';
class Command extends BaseCommand {
usage() {
return 'autocompletion <type> [arg1]';
}
description() {
return 'Helper for autocompletion';
}
options() {
return [
[ '--before <before>', 'before' ],
[ '--multiline', 'multiline' ],
];
}
hidden() {
return true;
}
async action(args) {
console.info(args);
let output = [];
if (args.type == 'notes') {
// TODO:
if (!app().currentFolder()) throw new Error('no current folder');
let options = {};
// this.log(JSON.stringify(['XX'+args.options.before+'XX', 'aa','bb']));
// return;
//console.info(args.options.before);
if (args.options.before) options.titlePattern = args.options.before + '*';
const notes = await Note.previews(app().currentFolder().id, options);
output = notes.map((n) => n.title);
}
if (args.options.multiline) {
output = output.map((s) => s.replace(/ /g, '\\ '));
this.log(output.join("\n"));
} else {
this.log(JSON.stringify(output));
}
}
}
module.exports = Command;

View File

@ -9,7 +9,7 @@ import { autocompleteItems } from './autocomplete.js';
class Command extends BaseCommand {
usage() {
return 'geoloc <title>';
return 'geoloc <note>';
}
description() {
@ -21,7 +21,7 @@ class Command extends BaseCommand {
}
async action(args) {
let title = args['title'];
let title = args['note'];
let item = await app().loadItem(BaseModel.TYPE_NOTE, title, { parent: app().currentFolder() });
if (!item) throw new Error(_('Cannot find "%s".', title));

View File

@ -13,11 +13,11 @@ import { cliUtils } from './cli-utils.js';
class Command extends BaseCommand {
usage() {
return 'ls [pattern]';
return 'ls [note-pattern]';
}
description() {
return _('Displays the notes in [notebook]. Use `ls /` to display the list of notebooks.');
return _('Displays the notes in the current notebook. Use `ls /` to display the list of notebooks.');
}
options() {
@ -36,7 +36,7 @@ class Command extends BaseCommand {
}
async action(args) {
let pattern = args['pattern'];
let pattern = args['note-pattern'];
let items = [];
let options = args.options;

View File

@ -7,7 +7,7 @@ import { reg } from 'lib/registry.js';
class Command extends BaseCommand {
usage() {
return 'mkbook <notebook>';
return 'mkbook <new-notebook>';
}
description() {
@ -19,7 +19,7 @@ class Command extends BaseCommand {
}
async action(args) {
let folder = await Folder.save({ title: args['notebook'] }, { userSideValidation: true });
let folder = await Folder.save({ title: args['new-notebook'] }, { userSideValidation: true });
app().switchCurrentFolder(folder);
}

View File

@ -6,7 +6,7 @@ import { Note } from 'lib/models/note.js';
class Command extends BaseCommand {
usage() {
return 'mknote <note>';
return 'mknote <new-note>';
}
description() {
@ -17,7 +17,7 @@ class Command extends BaseCommand {
if (!app().currentFolder()) throw new Error(_('Notes can only be created within a notebook.'));
let note = {
title: args.note,
title: args['new-note'],
parent_id: app().currentFolder().id,
};

View File

@ -6,7 +6,7 @@ import { Note } from 'lib/models/note.js';
class Command extends BaseCommand {
usage() {
return 'mktodo <note>';
return 'mktodo <new-todo>';
}
description() {
@ -17,7 +17,7 @@ class Command extends BaseCommand {
if (!app().currentFolder()) throw new Error(_('Notes can only be created within a notebook.'));
let note = {
title: args.note,
title: args['new-todo'],
parent_id: app().currentFolder().id,
is_todo: 1,
};

View File

@ -9,11 +9,11 @@ import { autocompleteItems } from './autocomplete.js';
class Command extends BaseCommand {
usage() {
return 'mv <pattern> <destination>';
return 'mv <note-pattern> [notebook]';
}
description() {
return _('Moves the notes matching <pattern> to <destination>. If <pattern> is a note, it will be moved to the notebook <destination>. If <pattern> is a notebook, it will be renamed to <destination>.');
return _('Moves the notes matching <note-pattern> to [notebook].');
}
autocomplete() {
@ -21,26 +21,17 @@ class Command extends BaseCommand {
}
async action(args) {
const pattern = args['pattern'];
const destination = args['destination'];
const pattern = args['note-pattern'];
const destination = args['notebook'];
const folder = await Folder.loadByField('title', destination);
if (!folder) throw new Error(_('Cannot find "%s".', destination));
const item = await app().guessTypeAndLoadItem(pattern);
const notes = await app().loadItems(BaseModel.TYPE_NOTE, pattern);
if (!notes.length) throw new Error(_('Cannot find "%s".', pattern));
if (!item) throw new Error(_('Cannot find "%s".', pattern));
if (item.type_ == BaseModel.TYPE_FOLDER) {
await Folder.save({ id: item.id, title: destination }, { userSideValidation: true });
await app().refreshCurrentFolder();
} else { // TYPE_NOTE
const folder = await Folder.loadByField('title', destination);
if (!folder) throw new Error(_('Cannot find "%s".', destination));
const notes = await app().loadItems(BaseModel.TYPE_NOTE, pattern);
if (!notes.length) throw new Error(_('Cannot find "%s".', pattern));
for (let i = 0; i < notes.length; i++) {
await Note.moveToFolder(notes[i].id, folder.id);
}
for (let i = 0; i < notes.length; i++) {
await Note.moveToFolder(notes[i].id, folder.id);
}
}

View File

@ -10,11 +10,11 @@ import { autocompleteItems } from './autocomplete.js';
class Command extends BaseCommand {
usage() {
return 'rm <pattern>';
return 'rm <note-pattern>';
}
description() {
return _('Deletes the items matching <pattern>.');
return _('Deletes the notes matching <note-pattern>.');
}
autocomplete() {
@ -24,30 +24,29 @@ class Command extends BaseCommand {
options() {
return [
['-f, --force', _('Deletes the items without asking for confirmation.')],
['-r, --recursive', _('Deletes a notebook.')],
];
}
async action(args) {
const pattern = args['pattern'].toString();
const pattern = args['note-pattern'];
const recursive = args.options && args.options.recursive === true;
const force = true || args.options && args.options.force === true; // TODO
if (recursive) {
const folder = await app().loadItem(BaseModel.TYPE_FOLDER, pattern);
if (!folder) throw new Error(_('Cannot find "%s".', pattern));
//const ok = force ? true : await vorpalUtils.cmdPromptConfirm(this, _('Delete notebook "%s"?', folder.title));
if (!ok) return;
await Folder.delete(folder.id);
await app().refreshCurrentFolder();
} else {
const notes = await app().loadItems(BaseModel.TYPE_NOTE, pattern);
if (!notes.length) throw new Error(_('Cannot find "%s".', pattern));
const ok = force ? true : await vorpalUtils.cmdPromptConfirm(this, _('%d notes match this pattern. Delete them?', notes.length));
if (!ok) return;
let ids = notes.map((n) => n.id);
await Note.batchDelete(ids);
}
// if (recursive) {
// const folder = await app().loadItem(BaseModel.TYPE_FOLDER, pattern);
// if (!folder) throw new Error(_('Cannot find "%s".', pattern));
// //const ok = force ? true : await vorpalUtils.cmdPromptConfirm(this, _('Delete notebook "%s"?', folder.title));
// if (!ok) return;
// await Folder.delete(folder.id);
// await app().refreshCurrentFolder();
// } else {
const notes = await app().loadItems(BaseModel.TYPE_NOTE, pattern);
if (!notes.length) throw new Error(_('Cannot find "%s".', pattern));
const ok = force ? true : await vorpalUtils.cmdPromptConfirm(this, _('%d notes match this pattern. Delete them?', notes.length));
if (!ok) return;
let ids = notes.map((n) => n.id);
await Note.batchDelete(ids);
}
}

View File

@ -21,6 +21,10 @@ class Command extends BaseCommand {
return { data: autocompleteItems };
}
hidden() {
return true;
}
async action(args) {
let title = args['note'];
let propName = args['name'];

View File

@ -7,11 +7,11 @@ import { BaseModel } from 'lib/base-model.js';
class Command extends BaseCommand {
usage() {
return 'tag <command> [tag] [note]';
return 'tag <tag-command> [tag] [note]';
}
description() {
return _('<command> can be "add", "remove" or "list" to assign or remove [tag] from [note], or to list the notes associated with [tag]. The command `tag list` can be used to list all the tags.');
return _('<tag-command> can be "add", "remove" or "list" to assign or remove [tag] from [note], or to list the notes associated with [tag]. The command `tag list` can be used to list all the tags.');
}
async action(args) {
@ -22,21 +22,23 @@ class Command extends BaseCommand {
notes = await app().loadItems(BaseModel.TYPE_NOTE, args.note);
}
if (args.command == 'remove' && !tag) throw new Error(_('Cannot find "%s".', args.tag));
const command = args['tag-command'];
if (args.command == 'add') {
if (command == 'remove' && !tag) throw new Error(_('Cannot find "%s".', args.tag));
if (command == 'add') {
if (!notes.length) throw new Error(_('Cannot find "%s".', args.note));
if (!tag) tag = await Tag.save({ title: args.tag });
for (let i = 0; i < notes.length; i++) {
await Tag.addNote(tag.id, notes[i].id);
}
} else if (args.command == 'remove') {
} else if (command == 'remove') {
if (!tag) throw new Error(_('Cannot find "%s".', args.tag));
if (!notes.length) throw new Error(_('Cannot find "%s".', args.note));
for (let i = 0; i < notes.length; i++) {
await Tag.removeNote(tag.id, notes[i].id);
}
} else if (args.command == 'list') {
} else if (command == 'list') {
if (tag) {
let notes = await Tag.notes(tag.id);
notes.map((note) => { this.log(note.title); });
@ -45,7 +47,7 @@ class Command extends BaseCommand {
tags.map((tag) => { this.log(tag.title); });
}
} else {
throw new Error(_('Invalid command: "%s"', args.command));
throw new Error(_('Invalid command: "%s"', command));
}
}

View File

@ -10,11 +10,11 @@ import { autocompleteItems } from './autocomplete.js';
class Command extends BaseCommand {
usage() {
return 'todo <action> <pattern>';
return 'todo <todo-command> <note-pattern>';
}
description() {
return _('<action> can either be "toggle" or "clear". Use "toggle" to toggle the given todo between completed and uncompleted state (If the target is a regular note it will be converted to a todo). Use "clear" to convert the todo back to a regular note.');
return _('<todo-command> can either be "toggle" or "clear". Use "toggle" to toggle the given todo between completed and uncompleted state (If the target is a regular note it will be converted to a todo). Use "clear" to convert the todo back to a regular note.');
}
autocomplete() {
@ -22,8 +22,8 @@ class Command extends BaseCommand {
}
async action(args) {
const action = args.action;
const pattern = args.pattern;
const action = args['todo-command'];
const pattern = args['note-pattern'];
const notes = await app().loadItems(BaseModel.TYPE_NOTE, pattern);
if (!notes.length) throw new Error(_('Cannot find "%s".', pattern));

View File

@ -7,6 +7,7 @@ mkdir -p "$CLIENT_DIR/build"
rm -f "$CLIENT_DIR/app/lib"
ln -s "$CLIENT_DIR/../ReactNativeClient/lib" "$CLIENT_DIR/app"
cp "$CLIENT_DIR/package.json" "$CLIENT_DIR/build"
cp "$CLIENT_DIR/app/autocompletion_template.txt" "$CLIENT_DIR/build"
npm run build
#yarn run build