This commit is contained in:
Dmitry Vasilev
2023-07-02 20:22:41 +03:00
parent cc7339268b
commit f2dba93c7a
8 changed files with 197 additions and 87 deletions

View File

@@ -200,16 +200,10 @@
}
.allow_file_access {
height: 100%;
padding: 10px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
}
.allow_file_access .subtitle {
padding: 10px;
font-size: 0.8em;
}

View File

@@ -808,7 +808,7 @@ const clear_io_trace = state => {
return run_code({...state, io_trace: null})
}
const do_load_dir = (state, dir) => {
const load_files = (state, dir) => {
const collect_files = dir => dir.kind == 'file'
? [dir]
: dir.children.map(collect_files).flat()
@@ -820,7 +820,7 @@ const do_load_dir = (state, dir) => {
return {
...state,
project_dir: dir,
files: {...files, ...state.files},
files: {...files, '': state.files['']},
}
}
@@ -842,24 +842,29 @@ const apply_entrypoint_settings = (state, entrypoint_settings) => {
}
}
const load_dir = (state, dir, entrypoint_settings) => {
const load_dir = (state, dir, has_file_system_access, entrypoint_settings) => {
// Clear parse cache and rerun code
const with_dir = do_load_dir(state, dir)
const with_dir = load_files(state, dir)
return run_code({
...(
entrypoint_settings == null
? with_dir
: apply_entrypoint_settings(with_dir, entrypoint_settings)
),
has_file_system_access,
// remove cache. We have to clear cache because imports of modules that are
// not available because project_dir is not available have errors and the
// errors are cached
parse_result: null,
external_imports_cache: null,
})
}
const create_file = (state, dir, current_module) => {
return {...load_dir(state, dir), current_module}
return {...load_dir(state, dir, true), current_module}
}
const open_run_window = (state, globals) => {
@@ -877,7 +882,7 @@ const open_run_window = (state, globals) => {
const get_initial_state = (state, entrypoint_settings) => {
const with_files = state.project_dir == null
? state
: do_load_dir(state, state.project_dir)
: load_files(state, state.project_dir)
const with_settings = apply_entrypoint_settings(with_files, entrypoint_settings)

View File

@@ -21,7 +21,7 @@ export function el(tag, className, ...children){
const append = child => {
if(typeof(child) == 'undefined') {
throw new Error('illegal state')
} else if(child !== null) {
} else if(child !== null && child !== false) {
result.appendChild(
typeof(child) == 'string'
? document.createTextNode(child)

View File

@@ -1,7 +1,13 @@
import {el} from './domutils.js'
import {map_find} from '../utils.js'
import {load_dir, create_file} from '../filesystem.js'
import {exec, get_state, open_directory, reload_run_window} from '../index.js'
import {open_dir, create_file} from '../filesystem.js'
import {
exec,
get_state,
open_directory,
reload_run_window,
close_directory,
} from '../index.js'
const is_html = path => path.endsWith('.htm') || path.endsWith('.html')
const is_js = path => path == '' || path.endsWith('.js') || path.endsWith('.mjs')
@@ -25,27 +31,56 @@ export class Files {
reload_run_window(get_state())
}
render(state) {
if(state.project_dir == null) {
this.el.innerHTML = ''
this.el.appendChild(
el('div', 'allow_file_access',
el('a', {
href: 'javascript:void(0)',
click: open_directory,
},
`Allow access to local project folder`,
),
el('div', 'subtitle', `Your files will never leave your device`)
)
)
} else {
this.render_files(state)
}
}
render_files(state) {
const children = [
render(state) {
const file_actions = state.has_file_system_access
? el('div', 'file_actions',
el('a', {
'class': 'file_action',
href: 'javascript: void(0)',
click: this.create_file.bind(this, false),
},
'New file'
),
el('a', {
'class': 'file_action',
href: 'javascript: void(0)',
click: this.create_file.bind(this, true),
},
'New dir'
),
el('a', {
'class': 'file_action',
href: 'javascript: void(0)',
click: close_directory,
},
'Revoke access'
),
el('a', {
href: 'https://github.com/leporello-js/leporello-js#selecting-entrypoint-module',
target: '__blank',
"class": 'select_entrypoint_title',
title: 'Select entrypoint',
},
'Entry point'
),
)
: el('div', 'file_actions',
el('div', 'file_action allow_file_access',
el('a', {
href: 'javascript: void(0)',
click: open_directory,
}, 'Allow access to local project folder'),
el('span', 'subtitle', `Your files will never leave your device`)
),
)
const file_elements = [
this.render_file({name: '*scratch*', path: ''}, state),
this.render_file(state.project_dir, state),
]
@@ -54,41 +89,19 @@ export class Files {
if(files == null) {
this.el.innerHTML = ''
this.el.appendChild(file_actions)
this.el.appendChild(
el('div', 'file_actions',
el('a', {
'class': 'file_action',
href: 'javascript: void(0)',
click: this.create_file.bind(this, false),
},
'Create file'
),
el('a', {
'class': 'file_action',
href: 'javascript: void(0)',
click: this.create_file.bind(this, true),
}, 'Create dir'),
el('a', {
href: 'https://github.com/leporello-js/leporello-js#selecting-entrypoint-module',
target: '__blank',
"class": 'select_entrypoint_title',
title: 'Select entrypoint',
}, 'Entry point'),
)
)
this.el.appendChild(
el('div', 'files',
children
)
el('div', 'files', file_elements)
)
} else {
// Replace to preserve scroll position
files.replaceChildren(...children)
this.el.replaceChild(file_actions, this.el.children[0])
files.replaceChildren(...file_elements)
}
}
render_select_entrypoint(file, state) {
if(file.kind == 'directory') {
if(!state.has_file_system_access || file.kind == 'directory') {
return null
} else if(is_js(file.path)) {
return el('span', 'select_entrypoint',
@@ -189,9 +202,9 @@ export class Files {
await create_file(path, is_dir)
// Reload all files for simplicity
load_dir(false).then(dir => {
open_dir(false).then(dir => {
if(is_dir) {
exec('load_dir', dir)
exec('load_dir', dir, true)
} else {
exec('create_file', dir, path)
}
@@ -205,8 +218,15 @@ export class Files {
this.active_el = e.currentTarget.parentElement
e.currentTarget.classList.add('active')
this.active_file = file
if(file.kind != 'directory') {
exec('change_current_module', file.path)
if(get_state().has_file_system_access) {
exec('change_current_module', file.path)
} else {
// in examples mode, on click file we also change entrypoint for
// simplicity
exec('change_entrypoint', file.path)
}
}
}
}

9
src/effects.js vendored
View File

@@ -1,4 +1,5 @@
import {write_file} from './filesystem.js'
import {write_example} from './examples.js'
import {color_file} from './color.js'
import {
root_calltree_node,
@@ -293,6 +294,12 @@ export const EFFECTS = {
localStorage[key] = value
},
write: (state, [name, contents], ui) => write_file(name, contents),
write: (state, [name, contents], ui) => {
if(state.has_file_system_access) {
write_file(name, contents)
} else {
write_example(name, contents)
}
}
}

57
src/examples.js Normal file
View File

@@ -0,0 +1,57 @@
export const write_example = (name, contents) => {
localStorage['examples_' + name] = contents
}
const read_example = name => {
return localStorage['examples_' + name]
}
const list = [
'github_api/index.js',
'ethers/block_by_timestamp.js',
'ethers/index.js',
// TODO for html5 example, open run window or hint that it should be opened
]
.map(l => l.split('/'))
const get_children = path => {
const children = list.filter(l => path.every((elem, i) => elem == l[i] ))
const files = children.filter(c => c.length == path.length + 1)
const dirs = [...new Set(children
.filter(c => c.length != path.length + 1)
.map(c => c[path.length])
)]
return Promise.all(files.map(async f => {
const name = f[path.length]
const filepath = f.slice(0, path.length + 1).join('/')
return {
name,
path: filepath,
kind: 'file',
contents:
read_example(filepath) ??
await fetch(globalThis.location.origin + '/docs/examples/'+ filepath)
.then(r => r.text()),
}
})
.concat(dirs.map(async d => {
const p = [...path, d]
return {
name: d,
path: p.join('/'),
kind: 'directory',
children: await get_children(p),
}
})))
}
export const examples_promise = get_children([]).then(children => {
return {
kind: 'directory',
name: 'examples',
path: null,
children,
}
})

View File

@@ -15,11 +15,10 @@ const send_message = (message) => {
});
}
globalThis.clear_directory_handle = () => {
export const close_dir = () => {
send_message({type: 'SET_DIR_HANDLE', data: null})
clearInterval(keepalive_interval_id)
keepalive_interval_id = null
window.location.reload()
}
let dir_handle
@@ -120,7 +119,7 @@ const read_file = async handle => {
return await file_data.text()
}
const do_load_dir = async (handle, path) => {
const do_open_dir = async (handle, path) => {
if(handle.kind == 'directory') {
const children = []
for await (let [name, h] of handle) {
@@ -134,7 +133,7 @@ const do_load_dir = async (handle, path) => {
kind: 'directory',
children: (await Promise.all(
children.map(c =>
do_load_dir(c, path == null ? c.name : path + '/' + c.name)
do_open_dir(c, path == null ? c.name : path + '/' + c.name)
)
)).sort((a, b) => a.name.localeCompare(b.name))
}
@@ -159,7 +158,7 @@ export const create_file = (path, is_dir) => {
)
}
export const load_dir = async (should_request_access) => {
export const open_dir = async (should_request_access) => {
let handle
if(should_request_access) {
handle = await request_directory_handle()
@@ -171,5 +170,5 @@ export const load_dir = async (should_request_access) => {
} else {
keep_service_worker_alive()
}
return do_load_dir(handle, null)
return do_open_dir(handle, null)
}

View File

@@ -1,12 +1,22 @@
import {UI} from './editor/ui.js'
import {EFFECTS, render_initial_state, apply_side_effects} from './effects.js'
import {load_dir, init_window_service_worker} from './filesystem.js'
import {
open_dir,
close_dir,
init_window_service_worker
} from './filesystem.js'
import {examples_promise} from './examples.js'
const EXAMPLE = `const fib = n =>
n == 0 || n == 1
? n
: fib(n - 1) + fib(n - 2)
fib(6)`
const EXAMPLE = `function fib(n) {
if(n == 0 || n == 1) {
return n
} else {
return fib(n - 1) + fib(n - 2)
}
}
fib(6)
`
const set_error_handler = (w, with_unhandled_rejection = true) => {
@@ -131,38 +141,56 @@ export const reload_run_window = state => {
globalThis.run_window.location = get_html_url(state)
}
const read_modules = async () => {
const default_module = {'': localStorage.code || EXAMPLE}
const project_dir = await load_dir(false)
const project_dir = await open_dir(false)
if(project_dir == null) {
// Single anonymous module
return {
project_dir: await examples_promise,
files: default_module,
has_file_system_access: false,
}
} else {
return {
project_dir,
files: default_module,
has_file_system_access: true,
}
}
}
const get_entrypoint_settings = () => ({
current_module: localStorage.current_module ?? '',
entrypoint: localStorage.entrypoint ?? '',
html_file: localStorage.html_file ?? '',
})
const get_entrypoint_settings = () => {
const params = new URLSearchParams(window.location.search)
const entrypoint = null
?? params.get('entrypoint')
?? localStorage.entrypoint
?? ''
return {
current_module: params.get('entrypoint') ?? localStorage.current_module ?? '',
entrypoint,
html_file: localStorage.html_file ?? '',
}
}
export const open_directory = () => {
if(globalThis.showDirectoryPicker == null) {
throw new Error('Your browser is not supporting File System Access API')
}
load_dir(true).then(dir => {
exec('load_dir', dir, get_entrypoint_settings())
open_dir(true).then(dir => {
exec('load_dir', dir, true, get_entrypoint_settings())
})
}
export const close_directory = async () => {
close_dir()
exec('load_dir', await examples_promise, false, get_entrypoint_settings())
}
let COMMANDS
let ui