#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; using ScintillaNET; #endregion namespace CodeImp.DoomBuilder.Data.Scripting { [ScriptHandler(ScriptType.UNKNOWN)] internal 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 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 UpdateFunctionBarItems(ScriptDocumentTab tab, MemoryStream stream, ComboBox target) { // Unsupported script type. Just clear the items target.Items.Clear(); return new List(); } //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 filtered = new List(); 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 autocompletedict = new Dictionary(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 addedprops = new HashSet(); 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 result = new List(); 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(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; } } //mxd. If the tip obscures the view, move it down int tippos; int funcline = scriptedit.LineFromPosition(curfunctionstartpos); if(scriptedit.CurrentLine > funcline) tippos = scriptedit.Lines[scriptedit.CurrentLine].Position + scriptedit.Lines[scriptedit.CurrentLine].Indentation; //scriptedit.PositionFromLine(curline) /*+ (curfunctionstartpos - scriptedit.PositionFromLine(funcline))*/; else tippos = curfunctionstartpos; // Show tip scriptedit.CallTipShow(tippos, functiondef); scriptedit.CallTipSetHlt(highlightstart, highlightend); } } } #endregion } }