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 { DB_WRITE } = require("../../services/database/consts");
const { Context } = require("../../util/context"); const { Context } = require("../../util/context");
const { origin_from_url } = require("../../util/urlutil"); 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 { BaseES } = require("./BaseES");
const uuidv4 = require('uuid').v4; const uuidv4 = require('uuid').v4;
@ -34,13 +34,19 @@ class AppES extends BaseES {
const services = this.context.get('services'); const services = this.context.get('services');
this.db = services.get('database').get(DB_WRITE, 'apps'); this.db = services.get('database').get(DB_WRITE, 'apps');
}, },
async create_predicate (id) { async create_predicate (id, ...args) {
if ( id === 'user-can-edit' ) { if ( id === 'user-can-edit' ) {
return new Eq({ return new Eq({
key: 'owner', key: 'owner',
value: Context.get('user').id, value: Context.get('user').id,
}); });
} }
if ( id === 'name-like' ) {
return new Like({
key: 'name',
value: args[0],
});
}
}, },
async delete (uid, extra) { async delete (uid, extra) {
const svc_appInformation = this.context.get('services').get('app-information'); 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 APIError = require("../../api/APIError");
const { Entity } = require("./Entity"); const { Entity } = require("./Entity");
const { WeakConstructorTrait } = require("../../traits/WeakConstructorTrait"); 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"); const { DB_WRITE } = require("../../services/database/consts");
class RawCondition extends AdvancedBase { class RawCondition extends AdvancedBase {
@ -355,6 +355,22 @@ class SQLES extends BaseES {
return new RawCondition({ sql, values }); 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) { Predicate.prototype.and = function (other) {
return new And({ children: [this, other] }); return new And({ children: [this, other] });
} }
@ -105,4 +114,5 @@ module.exports = {
And, And,
Or, Or,
Eq, Eq,
Like,
}; };

View File

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

View File

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

View File

@ -31,4 +31,9 @@ export class BuiltinCommandProvider {
} }
return undefined; 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; if ( results.length === 0 ) return undefined;
return results; 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 * 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/>. * 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 child_process from "node:child_process";
import stream from "node:stream"; import stream from "node:stream";
import { signals } from '../../ansi-shell/signals.js'; import { signals } from '../../ansi-shell/signals.js';
@ -167,9 +167,9 @@ function makeCommand(id, executablePath) {
async function findCommandsInPath(id, ctx, firstOnly) { async function findCommandsInPath(id, ctx, firstOnly) {
const PATH = ctx.env['PATH']; const PATH = ctx.env['PATH'];
if (!PATH) if (!PATH || id.includes(path_.sep))
return; return;
const pathDirectories = PATH.split(':'); const pathDirectories = PATH.split(path_.delimiter);
const results = []; const results = [];
@ -200,4 +200,26 @@ export class PathCommandProvider {
async lookupAll(id, { ctx }) { async lookupAll(id, { ctx }) {
return findCommandsInPath(id, ctx, false); 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; 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; return undefined;
} }
async complete (query, { ctx }) {
// TODO: Implement this
return [];
}
} }