Merge pull request #336 from AtkinsSJ/app-tab-completion

Phoenix: Add tab-completion for command names
This commit is contained in:
Eric Dubé 2024-05-03 12:39:07 -04:00 committed by GitHub
commit 4d0e6b4772
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 121 additions and 19 deletions

View File

@ -23,7 +23,7 @@ const { AppUnderUserActorType } = require("../../services/auth/Actor");
const { DB_WRITE } = require("../../services/database/consts");
const { Context } = require("../../util/context");
const { origin_from_url } = require("../../util/urlutil");
const { Eq, Or } = require("../query/query");
const { Eq, Like, Or } = require("../query/query");
const { BaseES } = require("./BaseES");
const uuidv4 = require('uuid').v4;
@ -34,13 +34,19 @@ class AppES extends BaseES {
const services = this.context.get('services');
this.db = services.get('database').get(DB_WRITE, 'apps');
},
async create_predicate (id) {
async create_predicate (id, ...args) {
if ( id === 'user-can-edit' ) {
return new Eq({
key: 'owner',
value: Context.get('user').id,
});
}
if ( id === 'name-like' ) {
return new Like({
key: 'name',
value: args[0],
});
}
},
async delete (uid, extra) {
const svc_appInformation = this.context.get('services').get('app-information');

View File

@ -22,7 +22,7 @@ const { BaseES } = require("./BaseES");
const APIError = require("../../api/APIError");
const { Entity } = require("./Entity");
const { WeakConstructorTrait } = require("../../traits/WeakConstructorTrait");
const { And, Or, Eq, Predicate, Null, PredicateUtil } = require("../query/query");
const { And, Or, Eq, Like, Null, Predicate, PredicateUtil } = require("../query/query");
const { DB_WRITE } = require("../../services/database/consts");
class RawCondition extends AdvancedBase {
@ -355,6 +355,22 @@ class SQLES extends BaseES {
return new RawCondition({ sql, values });
}
if ( om_query instanceof Like ) {
const key = om_query.key;
let value = om_query.value;
const prop = this.om.properties[key];
value = await prop.sql_reference(value);
const options = prop.descriptor.sql ?? {};
const col_name = options.column_name ?? prop.name;
const sql = `${col_name} LIKE ?`;
const values = [value];
return new RawCondition({ sql, values });
}
}
}
}

View File

@ -50,6 +50,15 @@ class Eq extends Predicate {
}
}
class Like extends Predicate {
async check (entity) {
// Convert SQL LIKE pattern to RegExp
// TODO: Support escaping the pattern characters
const regex = new RegExp(this.value.replaceAll('%', '.*').replaceAll('_', '.'), 'i');
return regex.test(await entity.get(this.key));
}
}
Predicate.prototype.and = function (other) {
return new And({ children: [this, other] });
}
@ -105,4 +114,5 @@ module.exports = {
And,
Or,
Eq,
Like,
};

View File

@ -17,9 +17,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Context } from '../../context/context.js';
import { CommandCompleter } from '../../puter-shell/completers/command_completer.js';
import { FileCompleter } from '../../puter-shell/completers/file_completer.js';
import { OptionCompleter } from '../../puter-shell/completers/option_completer.js';
import { CommandCompleter } from '../../puter-shell/completers/CommandCompleter.js';
import { FileCompleter } from '../../puter-shell/completers/FileCompleter.js';
import { OptionCompleter } from '../../puter-shell/completers/OptionCompleter.js';
import { Uint8List } from '../../util/bytes.js';
import { StatefulProcessorBuilder } from '../../util/statemachine.js';
import { ANSIContext } from '../ANSIContext.js';

View File

@ -25,15 +25,12 @@ export class CommandCompleter {
return [];
}
const completions = [];
// TODO: Match executable names as well as builtins
for ( const commandName of Object.keys(builtins) ) {
if ( commandName.startsWith(query) ) {
completions.push(commandName.slice(query.length));
}
}
return completions;
return (await ctx.externs.commandProvider.complete(query, { ctx }))
// Remove any duplicate results
.filter((item, pos, self) => self.indexOf(item) === pos)
// TODO: Sort completions?
// Remove the `query` part of each result, as that's what is expected
// TODO: Supply whole results instead?
.map(it => it.slice(query.length));
}
}

View File

@ -31,4 +31,9 @@ export class BuiltinCommandProvider {
}
return undefined;
}
async complete (query) {
return Object.keys(builtins)
.filter(commandName => commandName.startsWith(query));
}
}

View File

@ -42,4 +42,15 @@ export class CompositeCommandProvider {
if ( results.length === 0 ) return undefined;
return results;
}
async complete (...a) {
const query = a[0];
if (query === '') return [];
const results = [];
for (const provider of this.providers) {
results.push(...await provider.complete(...a));
}
return results;
}
}

View File

@ -16,7 +16,7 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import path_ from "path-browserify";
import path_ from "node:path";
import child_process from "node:child_process";
import stream from "node:stream";
import { signals } from '../../ansi-shell/signals.js';
@ -167,9 +167,9 @@ function makeCommand(id, executablePath) {
async function findCommandsInPath(id, ctx, firstOnly) {
const PATH = ctx.env['PATH'];
if (!PATH)
if (!PATH || id.includes(path_.sep))
return;
const pathDirectories = PATH.split(':');
const pathDirectories = PATH.split(path_.delimiter);
const results = [];
@ -200,4 +200,26 @@ export class PathCommandProvider {
async lookupAll(id, { ctx }) {
return findCommandsInPath(id, ctx, false);
}
async complete(query, { ctx }) {
if (query === '') return [];
const PATH = ctx.env['PATH'];
if (!PATH)
return [];
const path_directories = PATH.split(path_.delimiter);
const results = [];
for (const dir of path_directories) {
const dir_entries = await ctx.platform.filesystem.readdir(dir);
for (const dir_entry of dir_entries) {
if (dir_entry.name.startsWith(query)) {
results.push(dir_entry.name);
}
}
}
return results;
}
}

View File

@ -114,4 +114,34 @@ export class PuterAppCommandProvider {
}
return undefined;
}
async complete (query, { ctx }) {
if (query === '') return [];
const results = [];
for (const app_name of BUILT_IN_APPS) {
if (app_name.startsWith(query)) {
results.push(app_name);
}
}
const request = await fetch(`${puter.APIOrigin}/drivers/call`, {
"headers": {
"Content-Type": "application/json",
"Authorization": `Bearer ${puter.authToken}`,
},
"body": JSON.stringify({ interface: 'puter-apps', method: 'select', args: { predicate: [ 'name-like', query + '%' ] } }),
"method": "POST",
});
const json = await request.json();
if (json.success) {
for (const app of json.result) {
results.push(app.name);
}
}
return results;
}
}

View File

@ -64,4 +64,9 @@ export class ScriptCommandProvider {
}
return undefined;
}
async complete (query, { ctx }) {
// TODO: Implement this
return [];
}
}