mirror of
https://git.do.srb2.org/STJr/UltimateZoneBuilder.git
synced 2024-11-30 15:41:30 +00:00
5eb438e3ba
Added map scripting capabilities to UDB (#656). Documentation at https://biwa.github.io/udbscript-docs/
455 lines
15 KiB
C#
Executable file
455 lines
15 KiB
C#
Executable file
#region ================== Namespaces
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Text;
|
|
using System.Windows.Forms;
|
|
using CodeImp.DoomBuilder.Compilers;
|
|
using CodeImp.DoomBuilder.Config;
|
|
using CodeImp.DoomBuilder.Controls;
|
|
using CodeImp.DoomBuilder.IO;
|
|
|
|
#endregion
|
|
|
|
using ScintillaNET;
|
|
|
|
namespace CodeImp.DoomBuilder.Data.Scripting
|
|
{
|
|
[ScriptHandler(ScriptType.UNKNOWN)]
|
|
public class ScriptHandler
|
|
{
|
|
#region ================== Constants
|
|
|
|
protected const int MAX_BACKTRACK_LENGTH = 200;
|
|
|
|
#endregion
|
|
|
|
#region ================== Variables
|
|
|
|
private ScriptEditorControl scriptcontrol;
|
|
protected Scintilla scriptedit;
|
|
protected ScriptConfiguration scriptconfig;
|
|
|
|
// List of keywords and constants
|
|
private List<string> autocompletelist;
|
|
|
|
// Current position information
|
|
private string curfunctionname = "";
|
|
private int curargumentindex;
|
|
private int curfunctionstartpos;
|
|
|
|
#endregion
|
|
|
|
#region ================== Methods
|
|
|
|
public virtual void Initialize(ScriptEditorControl scriptcontrol, ScriptConfiguration scriptconfig)
|
|
{
|
|
this.scriptcontrol = scriptcontrol;
|
|
this.scriptedit = scriptcontrol.Scintilla;
|
|
this.scriptconfig = scriptconfig;
|
|
|
|
// Bind events
|
|
this.scriptedit.KeyUp += scriptedit_KeyUp;
|
|
}
|
|
|
|
//TODO: Remove ScriptDocumentTab from here
|
|
public virtual List<CompilerError> UpdateFunctionBarItems(ScriptDocumentTab tab, MemoryStream stream, ComboBox target)
|
|
{
|
|
// Unsupported script type. Just clear the items
|
|
target.Items.Clear();
|
|
return new List<CompilerError>();
|
|
}
|
|
|
|
//mxd. Autocompletion handling (https://github.com/jacobslusser/ScintillaNET/wiki/Basic-Autocompletion)
|
|
public virtual bool ShowAutoCompletionList()
|
|
{
|
|
int currentpos = scriptedit.CurrentPosition;
|
|
int wordstartpos = scriptedit.WordStartPosition(currentpos, true);
|
|
|
|
if(wordstartpos >= currentpos)
|
|
{
|
|
// Hide the list
|
|
scriptedit.AutoCCancel();
|
|
return false;
|
|
}
|
|
|
|
// Get entered text
|
|
string start = scriptedit.GetTextRange(wordstartpos, currentpos - wordstartpos);
|
|
if(string.IsNullOrEmpty(start))
|
|
{
|
|
// Hide the list
|
|
scriptedit.AutoCCancel();
|
|
return false;
|
|
}
|
|
|
|
// Don't show Auto-completion list when editing comment, include or string
|
|
switch(scriptcontrol.GetScriptStyle(scriptedit.GetStyleAt(currentpos)))
|
|
{
|
|
case ScriptStyleType.Comment:
|
|
case ScriptStyleType.String:
|
|
// Hide the list
|
|
scriptedit.AutoCCancel();
|
|
return false;
|
|
|
|
case ScriptStyleType.Include:
|
|
// Hide the list unless current word is a keyword
|
|
if(!start.StartsWith("#"))
|
|
{
|
|
scriptedit.AutoCCancel();
|
|
return false;
|
|
}
|
|
break;
|
|
}
|
|
|
|
// Filter the list
|
|
List<string> filtered = new List<string>();
|
|
foreach(string s in autocompletelist)
|
|
if(s.IndexOf(start, StringComparison.OrdinalIgnoreCase) != -1) filtered.Add(s);
|
|
|
|
// Any matches?
|
|
if(filtered.Count > 0)
|
|
{
|
|
// Show the list
|
|
scriptedit.AutoCShow(currentpos - wordstartpos, string.Join(" ", filtered.ToArray()));
|
|
return true;
|
|
}
|
|
|
|
// Hide the list
|
|
scriptedit.AutoCCancel();
|
|
return false;
|
|
}
|
|
|
|
internal void SetKeywords(Configuration lexercfg, string lexername)
|
|
{
|
|
Dictionary<string, string> autocompletedict = new Dictionary<string, string>(StringComparer.Ordinal);
|
|
|
|
// Create the keywords list and apply it
|
|
string imageindex = ((int)ScriptEditorControl.ImageIndex.ScriptKeyword).ToString(CultureInfo.InvariantCulture);
|
|
int keywordsindex = lexercfg.ReadSetting(lexername + ".keywordsindex", -1);
|
|
if(keywordsindex > -1)
|
|
{
|
|
StringBuilder keywordslist = new StringBuilder();
|
|
foreach(string k in scriptconfig.Keywords)
|
|
{
|
|
if(keywordslist.Length > 0) keywordslist.Append(" ");
|
|
keywordslist.Append(k);
|
|
|
|
//mxd. Skip adding the keyword if we have a snippet with the same name
|
|
if(!scriptconfig.Snippets.Contains(k)) autocompletedict.Add(k, k + "?" + imageindex);
|
|
}
|
|
string words = keywordslist.ToString();
|
|
scriptedit.SetKeywords(keywordsindex, (scriptconfig.CaseSensitive ? words : words.ToLowerInvariant()));
|
|
}
|
|
|
|
//mxd. Create the properties list and apply it
|
|
imageindex = ((int)ScriptEditorControl.ImageIndex.ScriptProperty).ToString(CultureInfo.InvariantCulture);
|
|
int propertiesindex = lexercfg.ReadSetting(lexername + ".propertiesindex", -1);
|
|
if(propertiesindex > -1)
|
|
{
|
|
StringBuilder propertieslist = new StringBuilder();
|
|
HashSet<string> addedprops = new HashSet<string>();
|
|
char[] dot = { '.' };
|
|
foreach(string p in scriptconfig.Properties)
|
|
{
|
|
if(propertieslist.Length > 0) propertieslist.Append(" ");
|
|
|
|
// Scintilla doesn't highlight keywords with '.' or ':', so get rid of those
|
|
if(scriptconfig.ScriptType == ScriptType.DECORATE)
|
|
{
|
|
string prop = p;
|
|
if(prop.Contains(":")) prop = prop.Replace(":", string.Empty);
|
|
if(prop.Contains("."))
|
|
{
|
|
// Split dotted properties into separate entries
|
|
string[] parts = prop.Split(dot, StringSplitOptions.RemoveEmptyEntries);
|
|
List<string> result = new List<string>();
|
|
foreach(string part in parts)
|
|
{
|
|
if(!addedprops.Contains(part))
|
|
{
|
|
result.Add(part);
|
|
addedprops.Add(part);
|
|
}
|
|
}
|
|
|
|
if(result.Count > 0) propertieslist.Append(string.Join(" ", result.ToArray()));
|
|
}
|
|
else
|
|
{
|
|
addedprops.Add(prop);
|
|
propertieslist.Append(prop);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
propertieslist.Append(p);
|
|
}
|
|
|
|
// Autocomplete doesn't mind '.' or ':'
|
|
// Skip adding the keyword if we have a snippet with the same name
|
|
if(!scriptconfig.Snippets.Contains(p))
|
|
{
|
|
if(autocompletedict.ContainsKey(p))
|
|
General.ErrorLogger.Add(ErrorType.Warning, "Property \"" + p + "\" is double defined in \"" + scriptconfig.Description + "\" script configuration.");
|
|
else
|
|
autocompletedict.Add(p, p + "?" + imageindex);
|
|
}
|
|
}
|
|
string words = propertieslist.ToString();
|
|
scriptedit.SetKeywords(propertiesindex, (scriptconfig.CaseSensitive ? words : words.ToLowerInvariant()));
|
|
}
|
|
|
|
// Create the constants list and apply it
|
|
imageindex = ((int)ScriptEditorControl.ImageIndex.ScriptConstant).ToString(CultureInfo.InvariantCulture);
|
|
int constantsindex = lexercfg.ReadSetting(lexername + ".constantsindex", -1);
|
|
if(constantsindex > -1)
|
|
{
|
|
StringBuilder constantslist = new StringBuilder();
|
|
foreach(string c in scriptconfig.Constants)
|
|
{
|
|
if(autocompletedict.ContainsKey(c))
|
|
continue; //mxd. This happens when there's a keyword and a constant with the same name...
|
|
|
|
if(constantslist.Length > 0) constantslist.Append(" ");
|
|
constantslist.Append(c);
|
|
|
|
//mxd. Skip adding the constant if we have a snippet with the same name
|
|
if(!scriptconfig.Snippets.Contains(c)) autocompletedict.Add(c, c + "?" + imageindex);
|
|
}
|
|
string words = constantslist.ToString();
|
|
scriptedit.SetKeywords(constantsindex, (scriptconfig.CaseSensitive ? words : words.ToLowerInvariant()));
|
|
}
|
|
|
|
//mxd. Create the snippets list and apply it
|
|
imageindex = ((int)ScriptEditorControl.ImageIndex.ScriptSnippet).ToString(CultureInfo.InvariantCulture);
|
|
int snippetindex = lexercfg.ReadSetting(lexername + ".snippetindex", -1);
|
|
if(snippetindex > -1 && scriptconfig.Snippets.Count > 0)
|
|
{
|
|
StringBuilder snippetslist = new StringBuilder();
|
|
foreach(string s in scriptconfig.Snippets)
|
|
{
|
|
if(snippetslist.Length > 0) snippetslist.Append(" ");
|
|
snippetslist.Append(s);
|
|
autocompletedict.Add(s, s + "?" + imageindex);
|
|
}
|
|
string words = snippetslist.ToString();
|
|
scriptedit.SetKeywords(snippetindex, (scriptconfig.CaseSensitive ? words : words.ToLowerInvariant()));
|
|
}
|
|
|
|
// Make autocomplete list
|
|
autocompletelist = new List<string>(autocompletedict.Values);
|
|
}
|
|
|
|
// This gathers information about the current caret position
|
|
protected void UpdatePositionInfo()
|
|
{
|
|
int bracketlevel = 0; // bracket level counting
|
|
int argindex = 0; // function argument counting
|
|
int pos = scriptedit.CurrentPosition;
|
|
|
|
// Get the text
|
|
string scripttext = scriptedit.Text;
|
|
|
|
// Reset position info
|
|
curfunctionname = "";
|
|
curargumentindex = 0;
|
|
curfunctionstartpos = 0;
|
|
|
|
// Determine lowest backtrack position
|
|
int limitpos = Math.Max(0, scriptedit.CurrentPosition - MAX_BACKTRACK_LENGTH);
|
|
|
|
// We can only do this when we have function syntax information
|
|
if((scriptconfig.ArgumentDelimiter.Length == 0) || (scriptconfig.FunctionClose.Length == 0) ||
|
|
(scriptconfig.FunctionOpen.Length == 0) || (scriptconfig.Terminator.Length == 0))
|
|
return;
|
|
|
|
// Get int versions of the function syntax informantion
|
|
int argumentdelimiter = scriptconfig.ArgumentDelimiter[0];
|
|
int functionclose = scriptconfig.FunctionClose[0];
|
|
int functionopen = scriptconfig.FunctionOpen[0];
|
|
int terminator = scriptconfig.Terminator[0];
|
|
|
|
// Continue backtracking until we reached the limitpos
|
|
while(pos >= limitpos)
|
|
{
|
|
// Backtrack 1 character
|
|
pos--;
|
|
|
|
// Get the style and character at this position
|
|
ScriptStyleType curstyle = scriptcontrol.GetScriptStyle(scriptedit.GetStyleAt(pos));
|
|
int curchar = scriptedit.GetCharAt(pos);
|
|
|
|
// Then meeting ) then increase bracket level
|
|
// When meeting ( then decrease bracket level
|
|
// When bracket level goes -1, then the next word should be the function name
|
|
// Only when at bracket level 0, count the comma's for argument index
|
|
|
|
// TODO:
|
|
// Original code checked for scope character here and breaks if found
|
|
|
|
// Check if in plain text or keyword
|
|
if((curstyle == ScriptStyleType.PlainText) || (curstyle == ScriptStyleType.Keyword))
|
|
{
|
|
// Closing bracket
|
|
if(curchar == functionclose)
|
|
{
|
|
bracketlevel++;
|
|
}
|
|
// Opening bracket
|
|
else if(curchar == functionopen)
|
|
{
|
|
bracketlevel--;
|
|
|
|
// Out of the brackets?
|
|
if(bracketlevel < 0)
|
|
{
|
|
// Skip any whitespace before this bracket
|
|
do
|
|
{
|
|
// Backtrack 1 character
|
|
curchar = scriptedit.GetCharAt(--pos);
|
|
}
|
|
while((pos >= limitpos) && ((curchar == ' ') || (curchar == '\t') ||
|
|
(curchar == '\r') || (curchar == '\n')));
|
|
|
|
// NOTE: We may need to set onlyWordCharacters argument in the
|
|
// following calls to false to get any argument delimiter included,
|
|
// but this may also cause a valid keyword to be combined with other
|
|
// surrounding characters that do not belong to the keyword.
|
|
|
|
// Find the word before this bracket
|
|
int wordstart = scriptedit.WordStartPosition(pos, true);
|
|
int wordend = scriptedit.WordEndPosition(pos, true);
|
|
string word = scripttext.Substring(wordstart, wordend - wordstart);
|
|
if(word.Length > 0)
|
|
{
|
|
// Check if this is an argument delimiter
|
|
// I can't remember why I did this, but I'll probably stumble
|
|
// upon the problem if this doesn't work right (see note above)
|
|
if(word[0] == argumentdelimiter)
|
|
{
|
|
// We are now in the parent function
|
|
bracketlevel++;
|
|
argindex = 0;
|
|
}
|
|
// Now check if this is a keyword
|
|
else if(scriptconfig.IsKeyword(word))
|
|
{
|
|
// Found it!
|
|
curfunctionname = scriptconfig.GetKeywordCase(word);
|
|
curargumentindex = argindex;
|
|
curfunctionstartpos = wordstart;
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
// Don't know this word
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Argument delimiter
|
|
else if(curchar == argumentdelimiter)
|
|
{
|
|
// Only count these at brackt level 0
|
|
if(bracketlevel == 0) argindex++;
|
|
}
|
|
// Terminator
|
|
else if(curchar == terminator)
|
|
{
|
|
// Can't find anything, break now
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ================== Events
|
|
|
|
private void scriptedit_KeyUp(object sender, KeyEventArgs e)
|
|
{
|
|
bool showcalltip = false;
|
|
int highlightstart = 0;
|
|
int highlightend = 0;
|
|
|
|
UpdatePositionInfo();
|
|
|
|
// Call tip shown
|
|
if(scriptedit.CallTipActive)
|
|
{
|
|
// Should we hide the call tip?
|
|
if(curfunctionname.Length == 0)
|
|
{
|
|
// Hide the call tip
|
|
scriptedit.CallTipCancel();
|
|
}
|
|
else
|
|
{
|
|
// Update the call tip
|
|
showcalltip = true;
|
|
}
|
|
}
|
|
// No call tip
|
|
else
|
|
{
|
|
// Should we show a call tip?
|
|
showcalltip = (curfunctionname.Length > 0) && !scriptedit.AutoCActive;
|
|
}
|
|
|
|
// Show or update call tip
|
|
if(showcalltip)
|
|
{
|
|
string functiondef = scriptconfig.GetFunctionDefinition(curfunctionname);
|
|
if(functiondef != null)
|
|
{
|
|
// Determine the range to highlight
|
|
int argsopenpos = functiondef.IndexOf(scriptconfig.FunctionOpen, StringComparison.Ordinal);
|
|
int argsclosepos = functiondef.LastIndexOf(scriptconfig.FunctionClose, StringComparison.Ordinal);
|
|
if((argsopenpos > -1) && (argsclosepos > -1))
|
|
{
|
|
string argsstr = functiondef.Substring(argsopenpos + 1, argsclosepos - argsopenpos - 1);
|
|
string[] args = argsstr.Split(scriptconfig.ArgumentDelimiter[0]);
|
|
if((curargumentindex >= 0) && (curargumentindex < args.Length))
|
|
{
|
|
int argoffset = 0;
|
|
for(int i = 0; i < curargumentindex; i++) argoffset += args[i].Length + 1;
|
|
highlightstart = argsopenpos + argoffset + 1;
|
|
highlightend = highlightstart + args[curargumentindex].Length;
|
|
}
|
|
}
|
|
|
|
// Determine callip position
|
|
int tippos = curfunctionstartpos;
|
|
int funcline = scriptedit.LineFromPosition(curfunctionstartpos);
|
|
|
|
//mxd. If the tip obscures the view, move it down
|
|
if(scriptedit.CurrentLine > funcline)
|
|
{
|
|
int offset = curfunctionstartpos - scriptedit.Lines[funcline].Position;
|
|
tippos = scriptedit.Lines[scriptedit.CurrentLine].Position + offset;
|
|
}
|
|
|
|
//mxd. Take line wrapping into account
|
|
if(scriptedit.Lines[scriptedit.CurrentLine].WrapCount > 0)
|
|
{
|
|
int x = scriptedit.PointXFromPosition(tippos);
|
|
int y = scriptedit.PointYFromPosition(scriptedit.CurrentPosition);
|
|
int newtippos = scriptedit.CharPositionFromPointClose(x, y);
|
|
if(newtippos != -1) tippos = newtippos;
|
|
}
|
|
|
|
// Show tip
|
|
scriptedit.CallTipShow(tippos, functiondef);
|
|
scriptedit.CallTipSetHlt(highlightstart, highlightend);
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|