swish/commit

Copied upstream files

authorJan Wielemaker
Sun Dec 4 11:22:49 2016 +0100
committerJan Wielemaker
Sun Dec 4 11:22:49 2016 +0100
commit56f6f5bb27d80d5febd474c51c83aa799ee8346b
treebd2d48677867ccf96f86e5e81505472913feb42a
parent24b9a02d860741e8d8f7d9eabe8390bd7ddd5de1
Diff style: patch stat
diff --git a/lib/swish/examples.pl b/lib/swish/examples.pl
index ffbd464..6f3ed90 100644
--- a/lib/swish/examples.pl
+++ b/lib/swish/examples.pl
@@ -106,7 +106,7 @@ index_json(HREF, Dir, JSON) :-
 	read_file_to_json(File, JSON0),
 	maplist(add_href(HREF), JSON0, JSON).
 index_json(HREF, Dir, JSON) :-
-	string_concat(Dir, "/*.pl", Pattern),
+	string_concat(Dir, "/*.{pl,swinb}", Pattern),
 	expand_file_name(Pattern, Files),
 	maplist(ex_file_json(HREF), Files, JSON).
 
diff --git a/lib/swish/gitty.pl b/lib/swish/gitty.pl
index 07dba5d..bf8c29d 100644
--- a/lib/swish/gitty.pl
+++ b/lib/swish/gitty.pl
@@ -433,7 +433,7 @@ set_head(Store, File, Head) :-
 		 *	       DIFF		*
 		 *******************************/
 
-%%	gitty_diff(+Store, ?Hash1, +FileOrHash2, -Dict) is det.
+%%	gitty_diff(+Store, ?Hash1, +FileOrHash2OrData, -Dict) is det.
 %
 %	True if Dict representeds the changes   in Hash1 to FileOrHash2.
 %	If Hash1 is unbound,  it  is   unified  with  the  `previous` of
@@ -448,7 +448,19 @@ set_head(Store, File, Head) :-
 %	  data.  Only present of data has changed
 %	  - tags:_{added:AddedTags, deleted:DeletedTags}
 %	  If tags have changed, the added and deleted ones.
+%
+%	@arg	FileOrHash2OrData is a file name, hash or a term
+%		data(String) to compare a given string with a
+%		gitty version.
 
+gitty_diff(Store, C1, data(Data2), Dict) :- !,
+	must_be(atom, C1),
+	gitty_data(Store, C1, Data1, _Meta1),
+	(   Data1 \== Data2
+	->  udiff_string(Data1, Data2, UDIFF),
+	    Dict = json{data:UDIFF}
+	;   Dict = json{}
+	).
 gitty_diff(Store, C1, C2, Dict) :-
 	gitty_data(Store, C2, Data2, Meta2),
 	(   var(C1)
diff --git a/lib/swish/highlight.pl b/lib/swish/highlight.pl
index 2dffcf9..778a185 100644
--- a/lib/swish/highlight.pl
+++ b/lib/swish/highlight.pl
@@ -96,18 +96,25 @@ tokens_.
 %	the editor is not known.
 
 codemirror_change(Request) :-
+	call_cleanup(codemirror_change_(Request),
+		     check_unlocked).
+
+codemirror_change_(Request) :-
 	http_read_json_dict(Request, Change, []),
 	debug(cm(change), 'Change ~p', [Change]),
-	UUID = Change.uuid,
-	(   shadow_editor(Change, TB)
+	atom_string(UUID, Change.uuid),
+	catch(shadow_editor(Change, TB),
+	      cm(Reason), true),
+	(   var(Reason)
 	->  (	catch(apply_change(TB, Changed, Change.change),
 		      cm(outofsync), fail)
 	    ->  mark_changed(TB, Changed),
+		release_editor(UUID),
 		reply_json_dict(true)
 	    ;	destroy_editor(UUID),
 		change_failed(UUID, outofsync)
 	    )
-	;   change_failed(UUID, existence_error)
+	;   change_failed(UUID, Reason)
 	).
 
 change_failed(UUID, Reason) :-
@@ -177,9 +184,15 @@ insert([H|T], TB, ChPos0, ChPos, Changed) :-
 	insert(T, TB, ChPos2, ChPos, Changed).
 
 :- dynamic
-	current_editor/4,			% UUID, MemFile, Role, Time
-	editor_last_access/2,			% UUID, Time
-	xref_upto_data/1.			% UUID
+	current_editor/5,		% UUID, MemFile, Role, Lock, Time
+	editor_last_access/2,		% UUID, Time
+	xref_upto_data/1.		% UUID
+
+%%	create_editor(+UUID, -Editor, +Change) is det.
+%
+%	Create a new editor for source UUID   from Change. The editor is
+%	created  in  a  locked  state  and    must   be  released  using
+%	release_editor/1 before it can be publically used.
 
 create_editor(UUID, Editor, Change) :-
 	must_be(atom, UUID),
@@ -190,7 +203,16 @@ create_editor(UUID, Editor, Change) :-
 	;   Role = source
 	),
 	get_time(Now),
-	asserta(current_editor(UUID, Editor, Role, Now)).
+	mutex_create(Lock),
+	with_mutex(swish_create_editor,
+		   register_editor(UUID, Editor, Role, Lock, Now)), !.
+create_editor(UUID, Editor, _Change) :-
+	fetch_editor(UUID, Editor).
+
+register_editor(UUID, Editor, Role, Lock, Now) :-
+	\+ current_editor(UUID, _, _, _, _),
+	mutex_lock(Lock),
+	asserta(current_editor(UUID, Editor, Role, Lock, Now)).
 
 %%	current_highlight_state(?UUID, -State) is nondet.
 %
@@ -200,9 +222,10 @@ current_highlight_state(UUID,
 			highlight{data:Editor,
 				  role:Role,
 				  created:Created,
+				  lock:Lock,
 				  access:Access
 				 }) :-
-	current_editor(UUID, Editor, Role, Created),
+	current_editor(UUID, Editor, Role, Lock, Created),
 	(   editor_last_access(Editor, Access)
 	->  true
 	;   Access = Created
@@ -218,25 +241,28 @@ current_highlight_state(UUID,
 uuid_like(UUID) :-
 	split_string(UUID, "-", "", Parts),
 	maplist(string_length, Parts, [8,4,4,4,12]),
-	\+ current_editor(UUID, _, _, _).
+	\+ current_editor(UUID, _, _, _, _).
 
 %%	destroy_editor(+UUID)
 %
 %	Destroy source admin UUID: the shadow  text (a memory file), the
-%	XREF data and the module used for cross-referencing.
+%	XREF data and the module used  for cross-referencing. The editor
+%	must  be  acquired  using  fetch_editor/2    before  it  can  be
+%	destroyed.
 
 destroy_editor(UUID) :-
 	must_be(atom, UUID),
+	current_editor(UUID, Editor, _, Lock, _), !,
+	mutex_unlock(Lock),
 	retractall(xref_upto_data(UUID)),
 	retractall(editor_last_access(UUID, _)),
-	current_editor(UUID, Editor, _, _), !,
-	(   xref_source_id(Editor, SourceID)
+	(   xref_source_id(UUID, SourceID)
 	->  xref_clean(SourceID),
 	    destroy_state_module(UUID)
 	;   true
 	),
-	% destroy late to make xref_source_identifier/2 work.
-	retractall(current_editor(UUID, Editor, _, _)),
+	% destroy after xref_clean/1 to make xref_source_identifier/2 work.
+	retractall(current_editor(UUID, Editor, _, _, _)),
 	free_memory_file(Editor).
 destroy_editor(_).
 
@@ -272,69 +298,120 @@ gc_editors :-
 gc_editors :-
 	editor_max_idle_time(MaxIdle),
 	forall(garbage_editor(UUID, MaxIdle),
-	       destroy_old_editor(UUID)).
+	       destroy_garbage_editor(UUID)).
 
 garbage_editor(UUID, TimeOut) :-
 	get_time(Now),
-	current_editor(UUID, _TB, _Role, Created),
+	current_editor(UUID, _TB, _Role, _Lock, Created),
 	Now - Created > TimeOut,
 	(   editor_last_access(UUID, Access)
 	->  Now - Access > TimeOut
 	;   true
 	).
 
-destroy_old_editor(UUID) :-
-	with_mutex(swish_gc_editor,
-		   destroy_old_editor_sync(UUID)).
-
-destroy_old_editor_sync(UUID) :-
-	editor_max_idle_time(MaxIdle),
-	garbage_editor(UUID, MaxIdle), !,
-	debug(cm(gc), 'GC highlight state for ~q', [UUID]),
+destroy_garbage_editor(UUID) :-
+	fetch_editor(UUID, _TB), !,
 	destroy_editor(UUID).
-destroy_old_editor_sync(_).
+destroy_garbage_editor(_).
 
 %%	fetch_editor(+UUID, -MemFile) is semidet.
 %
-%	Fetch existing editor for source UUID. Make sure the last access
-%	time is updated to avoid concurrent GC of the editor.
+%	Fetch existing editor for source UUID.   Update  the last access
+%	time. After success, the editor is   locked and must be released
+%	using release_editor/1.
 
 fetch_editor(UUID, TB) :-
-	with_mutex(swish_gc_editor,
-		   ( current_editor(UUID, TB, _Role, _),
-		     update_access(UUID)
-		   )).
+	current_editor(UUID, TB, Role, Lock, _),
+	catch(mutex_lock(Lock), error(existence_error(mutex,_),_), fail),
+	debug(cm(lock), 'Locked ~p', [UUID]),
+	(   current_editor(UUID, TB, Role, Lock, _)
+	->  update_access(UUID)
+	;   mutex_unlock(Lock)
+	).
+
+release_editor(UUID) :-
+	current_editor(UUID, _TB, _Role, Lock, _),
+	debug(cm(lock), 'Unlocked ~p', [UUID]),
+	mutex_unlock(Lock).
+
+check_unlocked :-
+	check_unlocked(unknown).
+
+check_unlocked(Reason) :-
+	thread_self(Me),
+	current_editor(_UUID, _TB, _Role, Lock, _),
+	mutex_property(Lock, status(locked(Me, _Count))), !,
+	print_message(error, locked(Reason, Me)),
+	assertion(fail).
+check_unlocked(_).
+
+unlocked_editor(UUID) :-
+	thread_self(Me),
+	current_editor(UUID, _TB, _Role, Lock, _),
+	mutex_property(Lock, status(locked(Me, _Count))), !,
+	fail.
+unlocked_editor(_).
+
+%%	update_access(+UUID)
+%
+%	Update the registered last access. We only update if the time is
+%	behind for more than a minute.
 
 update_access(UUID) :-
 	get_time(Now),
-	retractall(editor_last_access(UUID, _)),
-	asserta(editor_last_access(UUID, Now)).
+	(   editor_last_access(UUID, Last),
+	    Now-Last < 60
+	->  true
+	;   retractall(editor_last_access(UUID, _)),
+	    asserta(editor_last_access(UUID, Now))
+	).
 
 :- multifile
 	prolog:xref_source_identifier/2,
-	prolog:xref_open_source/2.
+	prolog:xref_open_source/2,
+	prolog:xref_close_source/2.
 
 prolog:xref_source_identifier(UUID, UUID) :-
-	current_editor(UUID, _, _, _).
+	current_editor(UUID, _, _, _, _).
+
+%%	prolog:xref_open_source(+UUID, -Stream)
+%
+%	Open a source. As we cannot open   the same source twice we must
+%	lock  it.  As  of  7.3.32   this    can   be  done  through  the
+%	prolog:xref_close_source/2 hook. In older  versions   we  get no
+%	callback on the close, so we must leave the editor unlocked.
 
+:- if(current_predicate(prolog_source:close_source/3)).
 prolog:xref_open_source(UUID, Stream) :-
-	current_editor(UUID, TB, _Role, _), !,
+	fetch_editor(UUID, TB),
 	open_memory_file(TB, read, Stream).
 
+prolog:xref_close_source(UUID, Stream) :-
+	release_editor(UUID),
+	close(Stream).
+:- else.
+prolog:xref_open_source(UUID, Stream) :-
+	fetch_editor(UUID, TB),
+	open_memory_file(TB, read, Stream),
+	release_editor(UUID).
+:- endif.
 
 %%	codemirror_leave(+Request)
 %
-%	POST  handler  that  deals   with    destruction   of  the  XPCE
-%	source_buffer  associated  with  an  editor,   as  well  as  the
-%	associated cross-reference information.
+%	POST  handler  that  deals  with    destruction  of  our  mirror
+%	associated  with  an  editor,   as    well   as  the  associated
+%	cross-reference information.
 
 codemirror_leave(Request) :-
+	call_cleanup(codemirror_leave_(Request),
+		     check_unlocked).
+
+codemirror_leave_(Request) :-
 	http_read_json_dict(Request, Data, []),
 	(   atom_string(UUID, Data.get(uuid))
 	->  debug(cm(leave), 'Leaving editor ~p', [UUID]),
-	    (	current_editor(UUID, _, _, _)
-	    ->	forall(current_editor(UUID, _TB, _Role, _),
-		       with_mutex(swish_gc_editor, destroy_editor(UUID)))
+	    (	fetch_editor(UUID, _TB)
+	    ->	destroy_editor(UUID)
 	    ;	debug(cm(leave), 'No editor for ~p', [UUID])
 	    )
 	;   debug(cm(leave), 'No editor?? (data=~p)', [Data])
@@ -347,7 +424,7 @@ codemirror_leave(Request) :-
 
 mark_changed(MemFile, Changed) :-
 	(   Changed == true
-	->  current_editor(UUID, MemFile, _Role, _),
+	->  current_editor(UUID, MemFile, _Role, _, _),
 	    retractall(xref_upto_data(UUID))
 	;   true
 	).
@@ -357,39 +434,31 @@ mark_changed(MemFile, Changed) :-
 xref(UUID) :-
 	xref_upto_data(UUID), !.
 xref(UUID) :-
-	current_editor(UUID, MF, _Role, _),
-	xref_source_id(MF, SourceId),
-	xref_state_module(MF, Module),
-	xref_source(SourceId,
-		    [ silent(true),
-		      module(Module)
-		    ]),
-	asserta(xref_upto_data(UUID)).
-
-%%	xref_source_id(+TextBuffer, -SourceID) is det.
-%
-%	Find the object we need  to   examine  for cross-referencing. If
-%	this is an included file, this is the corresponding main file.
-
-%xref_source_id(TB, SourceId) :-
-%	get(TB, file, File), File \== @nil, !,
-%	get(File, absolute_path, Path0),
-%	absolute_file_name(Path0, Path),
-%	master_load_file(Path, [], Master),
-%	(   Master == Path
-%	->  SourceId = TB
-%	;   SourceId = Master
-%	).
-xref_source_id(TB, UUID) :-
-	current_editor(UUID, TB, _Role, _).
-
-%%	xref_state_module(+TB, -Module) is semidet.
+	setup_call_cleanup(
+	    fetch_editor(UUID, _TB),
+	    ( xref_source_id(UUID, SourceId),
+	      xref_state_module(UUID, Module),
+	      xref_source(SourceId,
+			  [ silent(true),
+			    module(Module)
+			  ]),
+	      asserta(xref_upto_data(UUID))
+	    ),
+	    release_editor(UUID)).
+
+%%	xref_source_id(+Editor, -SourceID) is det.
+%
+%	SourceID is the xref source  identifier   for  Editor. As we are
+%	using UUIDs we just use the editor.
+
+xref_source_id(UUID, UUID).
+
+%%	xref_state_module(+UUID, -Module) is semidet.
 %
 %	True if we must run the cross-referencing   in  Module. We use a
 %	temporary module based on the UUID of the source.
 
-xref_state_module(TB, UUID) :-
-	current_editor(UUID, TB, _Role, _),
+xref_state_module(UUID, UUID) :-
 	(   module_property(UUID, class(temporary))
 	->  true
 	;   set_module(UUID:class(temporary)),
@@ -418,13 +487,23 @@ destroy_state_module(_).
 %	editor.
 
 codemirror_tokens(Request) :-
+	setup_call_catcher_cleanup(
+	    true,
+	    codemirror_tokens_(Request),
+	    Reason,
+	    check_unlocked(Reason)).
+
+codemirror_tokens_(Request) :-
 	http_read_json_dict(Request, Data, []),
+	atom_string(UUID, Data.get(uuid)),
 	debug(cm(tokens), 'Asking for tokens: ~p', [Data]),
 	(   catch(shadow_editor(Data, TB), cm(Reason), true)
 	->  (   var(Reason)
-	    ->	enriched_tokens(TB, Data, Tokens),
+	    ->	call_cleanup(enriched_tokens(TB, Data, Tokens),
+			     release_editor(UUID)),
 		reply_json_dict(json{tokens:Tokens}, [width(0)])
-	    ;	change_failed(Data.uuid, Reason)
+	    ;	check_unlocked(Reason),
+		change_failed(UUID, Reason)
 	    )
 	;   reply_json_dict(json{tokens:[[]]})
 	),
@@ -432,7 +511,7 @@ codemirror_tokens(Request) :-
 
 
 enriched_tokens(TB, _Data, Tokens) :-		% source window
-	current_editor(UUID, TB, source, _), !,
+	current_editor(UUID, TB, source, _Lock, _), !,
 	xref(UUID),
 	server_tokens(TB, Tokens).
 enriched_tokens(TB, Data, Tokens) :-		% query window
@@ -468,7 +547,7 @@ json_source_id(String, SourceID) :-
 string_source_id(String, SourceID) :-
 	atom_string(SourceID, String),
 	(   fetch_editor(SourceID, _TB)
-	->  true
+	->  release_editor(SourceID)
 	;   true
 	).
 
@@ -500,9 +579,12 @@ shadow_editor(Data, TB) :-
 	    mark_changed(TB, true)
 	;   Changes = Data.get(changes)
 	->  (   debug(cm(change), 'Patch editor for ~p', [UUID]),
-		maplist(apply_change(TB, Changed), Changes)
+		catch(maplist(apply_change(TB, Changed), Changes), E,
+		      (release_editor(UUID), throw(E)))
 	    ->	true
-	    ;	throw(cm(out_of_sync))
+	    ;	release_editor(UUID),
+		assertion(unlocked_editor(UUID)),
+		throw(cm(out_of_sync))
 	    ),
 	    mark_changed(TB, Changed)
 	).
@@ -527,9 +609,8 @@ shadow_editor(_Data, _TB) :-
 %%	server_tokens(+Role) is det.
 %
 %	These predicates help debugging the   server side. show_mirror/0
-%	opens the XPCE editor,  which   simplifies  validation  that the
-%	server  copy  is  in  sync  with    the  client.  The  predicate
-%	server_tokens/1 dumps the token list.
+%	displays the text the server thinks is in the client editor. The
+%	predicate server_tokens/1 dumps the token list.
 %
 %	@arg	Role is one of =source= or =query=, expressing the role of
 %		the editor in the SWISH UI.
@@ -539,12 +620,12 @@ shadow_editor(_Data, _TB) :-
 	server_tokens/1.
 
 show_mirror(Role) :-
-	current_editor(_UUID, TB, Role, _), !,
+	current_editor(_UUID, TB, Role, _Lock, _), !,
 	memory_file_to_string(TB, String),
 	write(user_error, String).
 
 server_tokens(Role) :-
-	current_editor(_UUID, TB, Role, _), !,
+	current_editor(_UUID, TB, Role, _Lock, _), !,
 	enriched_tokens(TB, _{}, Tokens),
 	print_term(Tokens, [output(user_error)]).
 
@@ -554,7 +635,7 @@ server_tokens(Role) :-
 %		represents the tokens found in a single toplevel term.
 
 server_tokens(TB, GroupedTokens) :-
-	current_editor(UUID, TB, _Role, _),
+	current_editor(UUID, TB, _Role, _Lock, _),
 	setup_call_cleanup(
 	    open_memory_file(TB, read, Stream),
 	    ( set_stream_file(TB, Stream),
@@ -1002,13 +1083,13 @@ predicate_info(Module:Name/Arity, Key, Value) :-
 	functor(Head, Name, Arity),
 	predicate_property(system:Head, iso), !,
 	ignore(Module = system),
-	(   catch(predicate(Name, Arity, Summary, _, _), _, fail),
+	(   catch(once(predicate(Name, Arity, Summary, _, _)), _, fail),
 	    Key = summary,
 	    Value = Summary
 	;   Key = iso,
 	    Value = true
 	).
 predicate_info(_Module:Name/Arity, summary, Summary) :-
-	catch(predicate(Name, Arity, Summary, _, _), _, fail), !.
+	catch(once(predicate(Name, Arity, Summary, _, _)), _, fail), !.
 predicate_info(PI, summary, Summary) :-	% PlDoc
-	prolog:predicate_summary(PI, Summary).
+	once(prolog:predicate_summary(PI, Summary)).
diff --git a/lib/swish/page.pl b/lib/swish/page.pl
index 72b481c..8900aab 100644
--- a/lib/swish/page.pl
+++ b/lib/swish/page.pl
@@ -343,7 +343,7 @@ swish_logo(_Options) -->
 %	Add search box to the navigation bar
 
 search_form(Options) -->
-	html(div(class(['col-sm-3', 'col-md-3', 'pull-right']),
+	html(div(class(['pull-right']),
 		 \search_box(Options))).
 
 
diff --git a/lib/swish/render/graphviz.pl b/lib/swish/render/graphviz.pl
index c7da3f5..31dcdf1 100644
--- a/lib/swish/render/graphviz.pl
+++ b/lib/swish/render/graphviz.pl
@@ -478,7 +478,8 @@ attribute(html(Value), O) --> !,
 attribute(Name=html(Value), _, List, Tail) :-
 	atomic(Value), !,
 	format(codes(List,Tail), '~w=<~w>', [Name, Value]).
-attribute(Name=html(Term), _, List, Tail) :- !,
+attribute(Name=html(Term), _, List, Tail) :-
+	nonvar(Term), !,
 	phrase(html(Term), Tokens0),
 	delete(Tokens0, nl(_), Tokens),
 	with_output_to(string(HTML), print_html(Tokens)),
diff --git a/lib/swish/storage.pl b/lib/swish/storage.pl
index 6400c94..549ede2 100644
--- a/lib/swish/storage.pl
+++ b/lib/swish/storage.pl
@@ -51,6 +51,7 @@
 
 :- use_module(page).
 :- use_module(gitty).
+:- use_module(patch).
 :- use_module(config).
 :- use_module(search).
 
@@ -163,14 +164,19 @@ storage(put, Request) :-
 	->  gitty_data(Dir, File, Data, _OldMeta)
 	;   option(data(Data), Dict, "")
 	),
-	meta_data(Request, Dict, Meta),
+	meta_data(Request, Dir, Dict, Meta),
 	storage_url(File, URL),
-	gitty_update(Dir, File, Data, Meta, Commit),
-	debug(storage, 'Updated: ~p', [Commit]),
-	reply_json_dict(json{url:URL,
-			     file:File,
-			     meta:Commit.put(symbolic, "HEAD")
-			    }).
+	catch(gitty_update(Dir, File, Data, Meta, Commit),
+	      Error,
+	      true),
+	(   var(Error)
+	->  debug(storage, 'Updated: ~p', [Commit]),
+	    reply_json_dict(json{url:URL,
+				 file:File,
+				 meta:Commit.put(symbolic, "HEAD")
+			    })
+	;   update_error(Error, Dir, Data, File, URL)
+	).
 storage(delete, Request) :-
 	authentity(Request, Meta),
 	setting(directory, Dir),
@@ -178,6 +184,39 @@ storage(delete, Request) :-
 	gitty_update(Dir, File, "", Meta, _New),
 	reply_json_dict(true).
 
+%%	update_error(+Error, +Storage, +Data, +File, +URL)
+%
+%	If error signals an edit conflict, prepare an HTTP =|409
+%	Conflict|= page
+
+update_error(error(gitty(commit_version(_, Head, Previous)), _),
+	     Dir, Data, File, URL) :- !,
+	gitty_diff(Dir, Previous, Head, OtherEdit),
+	gitty_diff(Dir, Previous, data(Data), MyEdits),
+	Status0 = json{url:URL,
+		       file:File,
+		       error:edit_conflict,
+		       edit:_{server:OtherEdit,
+			      me:MyEdits}
+		      },
+	(   OtherDiff = OtherEdit.get(data)
+	->  PatchOptions = [status(_), stderr(_)],
+	    patch(Data, OtherDiff, Merged, PatchOptions),
+	    Status1 = Status0.put(merged, Merged),
+	    foldl(patch_status, PatchOptions, Status1, Status)
+	;   Status = Status0
+	),
+	reply_json_dict(Status, [ status(409) ]).
+update_error(Error, _Dir, _Data, _File, _URL) :-
+	throw(Error).
+
+patch_status(status(exit(0)), Dict, Dict) :- !.
+patch_status(status(exit(Status)), Dict, Dict.put(patch_status, Status)) :- !.
+patch_status(status(killed(Signal)), Dict, Dict.put(patch_killed, Signal)) :- !.
+patch_status(stderr(""), Dict, Dict) :- !.
+patch_status(stderr(Errors), Dict, Dict.put(patch_errors, Errors)) :- !.
+
+
 request_file(Request, Dir, File) :-
 	option(path_info(File), Request),
 	(   gitty_file(Dir, File, _Hash)
@@ -189,7 +228,7 @@ storage_url(File, HREF) :-
 	http_link_to_id(web_storage, path_postfix(File), HREF).
 
 %%	meta_data(+Request, +Dict, -Meta) is det.
-%%	meta_data(+Request, Store, +Dict, -Meta) is det.
+%%	meta_data(+Request, +Store, +Dict, -Meta) is det.
 %
 %	Gather meta-data from the  Request   (user,  peer)  and provided
 %	meta-data. Illegal and unknown values are ignored.
diff --git a/lib/swish/swish_debug.pl b/lib/swish/swish_debug.pl
index ec995aa..89e2082 100644
--- a/lib/swish/swish_debug.pl
+++ b/lib/swish/swish_debug.pl
@@ -96,6 +96,10 @@ stale_module_property(M, thread_status, Status) :-
 	catch(thread_property(Thread, status(Status)), _, fail).
 stale_module_property(M, program_space, Space) :-
 	module_property(M, program_space(Space)).
+stale_module_property(M, program_size, Size) :-
+	module_property(M, program_size(Size)).
+stale_module_property(UUID, highlight_state, State) :-
+	current_highlight_state(UUID, State).
 
 
 %%	swish_statistics(?State)
diff --git a/lib/swish/trace.pl b/lib/swish/trace.pl
index 5ca9787..f7fb6cd 100644
--- a/lib/swish/trace.pl
+++ b/lib/swish/trace.pl
@@ -194,8 +194,6 @@ strip_stack(Error, Error).
 '$swish wrapper'(Goal, '$residuals'(Residuals)) :-
 	catch(swish_call(Goal), E, throw(E)),
 	deterministic(Det),
-	Goal = M:_,
-	residuals(M, Residuals),
 	(   tracing,
 	    Det == false
 	->  (   notrace,
@@ -205,7 +203,9 @@ strip_stack(Error, Error).
 		fail
 	    )
 	;   notrace
-	).
+	),
+	Goal = M:_,
+	residuals(M, Residuals).
 
 swish_call(Goal) :-
 	Goal,
diff --git a/web/bower_components/codemirror/mode/css/css.js b/web/bower_components/codemirror/mode/css/css.js
index ea7bd01..985287f 100644
--- a/web/bower_components/codemirror/mode/css/css.js
+++ b/web/bower_components/codemirror/mode/css/css.js
@@ -414,7 +414,7 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
   function keySet(array) {
     var keys = {};
     for (var i = 0; i < array.length; ++i) {
-      keys[array[i]] = true;
+      keys[array[i].toLowerCase()] = true;
     }
     return keys;
   }
@@ -494,7 +494,7 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
     "line-stacking-shift", "line-stacking-strategy", "list-style",
     "list-style-image", "list-style-position", "list-style-type", "margin",
     "margin-bottom", "margin-left", "margin-right", "margin-top",
-    "marker-offset", "marks", "marquee-direction", "marquee-loop",
+    "marks", "marquee-direction", "marquee-loop",
     "marquee-play-count", "marquee-speed", "marquee-style", "max-height",
     "max-width", "min-height", "min-width", "move-to", "nav-down", "nav-index",
     "nav-left", "nav-right", "nav-up", "object-fit", "object-position",
@@ -522,7 +522,7 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
     "text-wrap", "top", "transform", "transform-origin", "transform-style",
     "transition", "transition-delay", "transition-duration",
     "transition-property", "transition-timing-function", "unicode-bidi",
-    "vertical-align", "visibility", "voice-balance", "voice-duration",
+    "user-select", "vertical-align", "visibility", "voice-balance", "voice-duration",
     "voice-family", "voice-pitch", "voice-range", "voice-rate", "voice-stress",
     "voice-volume", "volume", "white-space", "widows", "width", "word-break",
     "word-spacing", "word-wrap", "z-index",
diff --git a/web/bower_components/codemirror/mode/javascript/javascript.js b/web/bower_components/codemirror/mode/javascript/javascript.js
index d7c5716..a717745 100644
--- a/web/bower_components/codemirror/mode/javascript/javascript.js
+++ b/web/bower_components/codemirror/mode/javascript/javascript.js
@@ -54,6 +54,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
         "namespace": C,
         "module": kw("module"),
         "enum": kw("module"),
+        "type": kw("type"),
 
         // scope modifiers
         "public": kw("modifier"),
@@ -208,6 +209,11 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
     var arrow = stream.string.indexOf("=>", stream.start);
     if (arrow < 0) return;
 
+    if (isTS) { // Try to skip TypeScript return type declarations after the arguments
+      var m = /:\s*(?:\w+(?:<[^>]*>|\[\])?|\{[^}]*\})\s*$/.exec(stream.string.slice(stream.start, arrow))
+      if (m) arrow = m.index
+    }
+
     var depth = 0, sawSomething = false;
     for (var pos = arrow - 1; pos >= 0; --pos) {
       var ch = stream.string.charAt(pos);
@@ -343,19 +349,19 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
 
   function statement(type, value) {
     if (type == "var") return cont(pushlex("vardef", value.length), vardef, expect(";"), poplex);
-    if (type == "keyword a") return cont(pushlex("form"), expression, statement, poplex);
+    if (type == "keyword a") return cont(pushlex("form"), parenExpr, statement, poplex);
     if (type == "keyword b") return cont(pushlex("form"), statement, poplex);
     if (type == "{") return cont(pushlex("}"), block, poplex);
     if (type == ";") return cont();
     if (type == "if") {
       if (cx.state.lexical.info == "else" && cx.state.cc[cx.state.cc.length - 1] == poplex)
         cx.state.cc.pop()();
-      return cont(pushlex("form"), expression, statement, poplex, maybeelse);
+      return cont(pushlex("form"), parenExpr, statement, poplex, maybeelse);
     }
     if (type == "function") return cont(functiondef);
     if (type == "for") return cont(pushlex("form"), forspec, statement, poplex);
     if (type == "variable") return cont(pushlex("stat"), maybelabel);
-    if (type == "switch") return cont(pushlex("form"), expression, pushlex("}", "switch"), expect("{"),
+    if (type == "switch") return cont(pushlex("form"), parenExpr, pushlex("}", "switch"), expect("{"),
                                       block, poplex, poplex);
     if (type == "case") return cont(expression, expect(":"));
     if (type == "default") return cont(expect(":"));
@@ -365,6 +371,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
     if (type == "export") return cont(pushlex("stat"), afterExport, poplex);
     if (type == "import") return cont(pushlex("stat"), afterImport, poplex);
     if (type == "module") return cont(pushlex("form"), pattern, pushlex("}"), expect("{"), block, poplex, poplex)
+    if (type == "type") return cont(typeexpr, expect("operator"), typeexpr, expect(";"));
     if (type == "async") return cont(statement)
     return pass(pushlex("stat"), expression, expect(";"), poplex);
   }
@@ -374,6 +381,10 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
   function expressionNoComma(type) {
     return expressionInner(type, true);
   }
+  function parenExpr(type) {
+    if (type != "(") return pass()
+    return cont(pushlex(")"), expression, expect(")"), poplex)
+  }
   function expressionInner(type, noComma) {
     if (cx.state.fatArrowAt == cx.stream.start) {
       var body = noComma ? arrowBodyNoComma : arrowBody;
@@ -384,6 +395,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
     var maybeop = noComma ? maybeoperatorNoComma : maybeoperatorComma;
     if (atomicTypes.hasOwnProperty(type)) return cont(maybeop);
     if (type == "function") return cont(functiondef, maybeop);
+    if (type == "class") return cont(pushlex("form"), classExpression, poplex);
     if (type == "keyword c" || type == "async") return cont(noComma ? maybeexpressionNoComma : maybeexpression);
     if (type == "(") return cont(pushlex(")"), maybeexpression, expect(")"), poplex, maybeop);
     if (type == "operator" || type == "spread") return cont(noComma ? expressionNoComma : expression);
@@ -519,11 +531,11 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
     if (type == "}") return cont();
     return pass(statement, block);
   }
-  function maybetype(type) {
-    if (isTS && type == ":") return cont(typeexpr);
-  }
-  function maybedefault(_, value) {
-    if (value == "=") return cont(expressionNoComma);
+  function maybetype(type, value) {
+    if (isTS) {
+      if (type == ":") return cont(typeexpr);
+      if (value == "?") return cont(maybetype);
+    }
   }
   function typeexpr(type) {
     if (type == "variable") {cx.marked = "variable-3"; return cont(afterType);}
@@ -606,24 +618,30 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
   }
   function funarg(type) {
     if (type == "spread") return cont(funarg);
-    return pass(pattern, maybetype, maybedefault);
+    return pass(pattern, maybetype, maybeAssign);
+  }
+  function classExpression(type, value) {
+    // Class expressions may have an optional name.
+    if (type == "variable") return className(type, value);
+    return classNameAfter(type, value);
   }
   function className(type, value) {
     if (type == "variable") {register(value); return cont(classNameAfter);}
   }
   function classNameAfter(type, value) {
-    if (value == "extends") return cont(isTS ? typeexpr : expression, classNameAfter);
+    if (value == "extends" || value == "implements") return cont(isTS ? typeexpr : expression, classNameAfter);
     if (type == "{") return cont(pushlex("}"), classBody, poplex);
   }
   function classBody(type, value) {
     if (type == "variable" || cx.style == "keyword") {
-      if (value == "static") {
+      if ((value == "static" || value == "get" || value == "set" ||
+           (isTS && (value == "public" || value == "private" || value == "protected" || value == "readonly" || value == "abstract"))) &&
+          cx.stream.match(/^\s+[\w$\xa1-\uffff]/, false)) {
         cx.marked = "keyword";
         return cont(classBody);
       }
       cx.marked = "property";
-      if (value == "get" || value == "set") return cont(classGetterSetter, functiondef, classBody);
-      return cont(functiondef, classBody);
+      return cont(isTS ? classfield : functiondef, classBody);
     }
     if (value == "*") {
       cx.marked = "keyword";
@@ -632,10 +650,10 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
     if (type == ";") return cont(classBody);
     if (type == "}") return cont();
   }
-  function classGetterSetter(type) {
-    if (type != "variable") return pass();
-    cx.marked = "property";
-    return cont();
+  function classfield(type, value) {
+    if (value == "?") return cont(classfield)
+    if (type == ":") return cont(typeexpr, maybeAssign)
+    return pass(functiondef)
   }
   function afterExport(_type, value) {
     if (value == "*") { cx.marked = "keyword"; return cont(maybeFrom, expect(";")); }
@@ -704,14 +722,18 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
     indent: function(state, textAfter) {
       if (state.tokenize == tokenComment) return CodeMirror.Pass;
       if (state.tokenize != tokenBase) return 0;
-      var firstChar = textAfter && textAfter.charAt(0), lexical = state.lexical;
+      var firstChar = textAfter && textAfter.charAt(0), lexical = state.lexical, top
       // Kludge to prevent 'maybelse' from blocking lexical scope pops
       if (!/^\s*else\b/.test(textAfter)) for (var i = state.cc.length - 1; i >= 0; --i) {
         var c = state.cc[i];
         if (c == poplex) lexical = lexical.prev;
         else if (c != maybeelse) break;
       }
-      if (lexical.type == "stat" && firstChar == "}") lexical = lexical.prev;
+      while ((lexical.type == "stat" || lexical.type == "form") &&
+             (firstChar == "}" || ((top = state.cc[state.cc.length - 1]) &&
+                                   (top == maybeoperatorComma || top == maybeoperatorNoComma) &&
+                                   !/^[,\.=+\-*:?[\(]/.test(textAfter))))
+        lexical = lexical.prev;
       if (statementIndent && lexical.type == ")" && lexical.prev.type == "stat")
         lexical = lexical.prev;
       var type = lexical.type, closing = firstChar == type;