1 // -*- mode: c++; c-file-style: "linux"; c-basic-offset: 2; indent-tabs-mode: nil -*-
2 //
3 //  Copyright (C) 2009-2015 Andrej Vodopivec <andrej.vodopivec@gmail.com>
4 //  Copyright (C) 2015-2019 Gunter Königsmann     <wxMaxima@physikbuch.de>
5 //
6 //  This program is free software; you can redistribute it and/or modify
7 //  it under the terms of the GNU General Public License as published by
8 //  the Free Software Foundation; either version 2 of the License, or
9 //  (at your option) any later version.
10 //
11 //  This program is distributed in the hope that it will be useful,
12 //  but WITHOUT ANY WARRANTY; without even the implied warranty of
13 //  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 //  GNU General Public License for more details.
15 //
16 //
17 //  You should have received a copy of the GNU General Public License
18 //  along with this program; if not, write to the Free Software
19 //  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
20 //
21 //  SPDX-License-Identifier: GPL-2.0+
22 
23 /*! \file
24   This file defines the class AutoComplete.
25 
26   AutoComplete creates the list of autocompletions for a string and allows
27   dynamically appending maxima commands to this list as soon as they are defined.
28 */
29 
30 #include <wx/sstream.h>
31 #include "Autocomplete.h"
32 #include "Dirstructure.h"
33 #include "Version.h"
34 #include <wx/textfile.h>
35 #include <wx/filename.h>
36 #include <wx/xml/xml.h>
37 #include "ErrorRedirector.h"
38 #include <wx/wfstream.h>
39 #include <wx/txtstrm.h>
40 
AutoComplete(Configuration * configuration)41 AutoComplete::AutoComplete(Configuration *configuration)
42 {
43   m_configuration = configuration;
44 }
45 
ClearWorksheetWords()46 void AutoComplete::ClearWorksheetWords()
47 {
48   #ifdef HAVE_OPENMP_TASKS
49   #pragma omp critical (AutocompleteBuiltins)
50   #endif
51   m_worksheetWords.clear();
52 }
53 
ClearDemofileList()54 void AutoComplete::ClearDemofileList()
55 {
56   #ifdef HAVE_OPENMP_TASKS
57   #pragma omp critical (AutocompleteFiles)
58   #endif
59   m_wordList[demofile] = m_builtInDemoFiles;
60 }
61 
AddSymbols(wxString xml)62 void AutoComplete::AddSymbols(wxString xml)
63 {
64   #ifdef HAVE_OPENMP_TASKS
65   wxLogMessage(_("Starting a background task that compiles a new list of autocompletible maxima commands."));
66   #pragma omp task
67   #endif
68   AddSymbols_Backgroundtask(xml);
69 }
70 
AddSymbols_Backgroundtask(wxString xml)71 void AutoComplete::AddSymbols_Backgroundtask(wxString xml)
72 {
73   #ifdef HAVE_OPENMP_TASKS
74   #pragma omp critical (AutocompleteBuiltins)
75   #endif
76   {
77     wxXmlDocument xmldoc;
78     wxStringInputStream xmlStream(xml);
79     xmldoc.Load(xmlStream, wxT("UTF-8"));
80     wxXmlNode *node = xmldoc.GetRoot();
81     if(node != NULL)
82     {
83       wxXmlNode *children = node->GetChildren();
84       while (children != NULL)
85       {
86         if(children->GetType() == wxXML_ELEMENT_NODE)
87         {
88           if (children->GetName() == wxT("function"))
89           {
90             wxXmlNode *val = children->GetChildren();
91             if(val)
92             {
93               wxString name = val->GetContent();
94               AddSymbol_nowait(name, command);
95             }
96           }
97 
98           if (children->GetName() == wxT("template"))
99           {
100             wxXmlNode *val = children->GetChildren();
101             if(val)
102             {
103               wxString name = val->GetContent();
104               AddSymbol_nowait(name, tmplte);
105             }
106           }
107 
108           if (children->GetName() == wxT("unit"))
109           {
110             wxXmlNode *val = children->GetChildren();
111             if(val)
112             {
113               wxString name = val->GetContent();
114               AddSymbol_nowait(name, unit);
115             }
116           }
117 
118           if (children->GetName() == wxT("value"))
119           {
120             wxXmlNode *val = children->GetChildren();
121             if(val)
122             {
123               wxString name = val->GetContent();
124               AddSymbol_nowait(name, command);
125             }
126           }
127         }
128         children = children->GetNext();
129       }
130     }
131   }
132 }
AddWorksheetWords(wxArrayString wordlist)133 void AutoComplete::AddWorksheetWords(wxArrayString wordlist)
134 {
135   #ifdef HAVE_OPENMP_TASKS
136   #pragma omp critical (AutocompleteBuiltins)
137   #endif
138   {
139     wxArrayString::const_iterator it;
140     for (it = wordlist.begin(); it != wordlist.end(); ++it)
141       m_worksheetWords[*it] = 1;
142   }
143 }
144 
~AutoComplete()145 AutoComplete::~AutoComplete()
146 {
147   #ifdef HAVE_OPENMP_TASKS
148   #pragma omp taskwait
149   #endif
150 }
151 
LoadSymbols()152 void AutoComplete::LoadSymbols()
153 {
154   #ifdef HAVE_OPENMP_TASKS
155   wxLogMessage(_("Starting a background task that setups the autocomplete builtins list."));
156   #pragma omp task
157   #endif
158   BuiltinSymbols_BackgroundTask();
159   #ifdef HAVE_OPENMP_TASKS
160   wxLogMessage(_("Starting a background task that setups the autocompletable files list."));
161   #pragma omp task
162   #endif
163   LoadSymbols_BackgroundTask();
164 }
165 
BuiltinSymbols_BackgroundTask()166 void AutoComplete::BuiltinSymbols_BackgroundTask()
167 {
168   #ifdef HAVE_OPENMP_TASKS
169   #pragma omp critical (AutocompleteBuiltins)
170   #endif
171   {
172     m_wordList[command].Clear();
173     m_wordList[tmplte].Clear();
174     m_wordList[esccommand].Clear();
175     m_wordList[unit].Clear();
176 
177     LoadBuiltinSymbols();
178 
179     for(Configuration::StringHash::const_iterator it = m_configuration->m_escCodes.begin();
180         it != m_configuration->m_escCodes.end();
181         ++it)
182        m_wordList[esccommand].Add(it->first);
183     m_wordList[command].Sort();
184     m_wordList[tmplte].Sort();
185     m_wordList[unit].Sort();
186     m_wordList[esccommand].Sort();
187 
188     wxString line;
189 
190     /// Load private symbol list (do something different on Windows).
191     wxString privateList;
192     privateList = Dirstructure::Get()->UserAutocompleteFile();
193     wxLogMessage(wxString::Format(
194                    _("Trying to load a list of autocompletible symbols from file %s"),
195                    privateList.utf8_str()));
196     if (wxFileExists(privateList))
197     {
198       wxTextFile priv(privateList);
199 
200       priv.Open();
201 
202       wxRegEx function("^[fF][uU][nN][cC][tT][iI][oO][nN] *: *");
203       wxRegEx option  ("^[oO][pP][tT][iI][oO][nN] *: *");
204       wxRegEx templte ("^[tT][eE][mM][pP][lL][aA][tT][eE] *: *");
205       wxRegEx unt    ("^[uU][nN][iI][tT] *: *");
206       for (line = priv.GetFirstLine(); !priv.Eof(); line = priv.GetNextLine())
207       {
208         line.Trim(true);
209         line.Trim(false);
210         if(!line.StartsWith("#"))
211         {
212           if (function.Replace(&line, ""))
213             m_wordList[command].Add(line);
214           else if (option.Replace(&line, ""))
215             m_wordList[command].Add(line);
216           else if (templte.Replace(&line, ""))
217             m_wordList[tmplte].Add(FixTemplate(line));
218           else if (unt.Replace(&line, ""))
219             m_wordList[unit].Add(line);
220           else
221             wxLogMessage(privateList +
222                          wxString::Format(_(": Can't interpret line: %s")), line.utf8_str());
223         }
224       }
225       priv.Close();
226     }
227     else
228     {
229       SuppressErrorDialogs logNull;
230       wxFileOutputStream output(privateList);
231       if(output.IsOk())
232       {
233         wxTextOutputStream text(output);
234         text << "# This file allows users to add their own symbols\n";
235         text << "# to wxMaxima's autocompletion feature.\n";
236         text << "# If a useful built-in symbol of Maxima is lacking\n";
237         text << "# in wxMaxima's autocompletion please inform the wxMaxima\n";
238         text << "# maintainers about this!\n";
239         text << "# \n";
240         text << "# The format of the entries in this file is:\n";
241         text << "# FUNCTION: myfunction\n";
242         text << "# OPTION: myvariable\n";
243         text << "# UNIT: myunit\n";
244         text << "# Template: mycommand(<expr>, <x>)";
245         text.Flush();
246       }
247     }
248   }
249 }
250 
LoadSymbols_BackgroundTask()251 void AutoComplete::LoadSymbols_BackgroundTask()
252 {
253   #ifdef HAVE_OPENMP_TASKS
254   #pragma omp critical (AutocompleteFiles)
255   #endif
256   {
257     // Error dialogues need to be created by the foreground thread.
258     SuppressErrorDialogs suppressor;
259 
260     // Prepare a list of all built-in loadable files of maxima.
261     {
262       GetMacFiles_includingSubdirs maximaLispIterator (m_builtInLoadFiles);
263       wxString sharedir = m_configuration->MaximaShareDir();
264       sharedir.Replace("\n","");
265       sharedir.Replace("\r","");
266       if(sharedir.IsEmpty())
267         wxLogMessage(_("Seems like the package with the maxima share files isn't installed."));
268       else
269       {
270         wxFileName shareDir(sharedir + "/");
271         shareDir.MakeAbsolute();
272         wxLogMessage(
273           wxString::Format(
274             _("Autocompletion: Scanning %s recursively for loadable lisp files."),
275             shareDir.GetFullPath().utf8_str()));
276         wxDir maximadir(shareDir.GetFullPath());
277         if(maximadir.IsOpened())
278           maximadir.Traverse(maximaLispIterator); //todo
279       }
280       GetMacFiles userLispIterator (m_builtInLoadFiles);
281       wxFileName userDir(Dirstructure::Get()->UserConfDir() + "/");
282       userDir.MakeAbsolute();
283       wxDir maximauserfilesdir(userDir.GetFullPath());
284       wxLogMessage(
285         wxString::Format(
286           _("Autocompletion: Scanning %s for loadable lisp files."),
287           userDir.GetFullPath().utf8_str()));
288       if(maximauserfilesdir.IsOpened())
289         maximauserfilesdir.Traverse(userLispIterator);
290       wxLogMessage(
291         wxString::Format(
292           _("Found %li loadable files."),
293           (unsigned long)m_builtInLoadFiles.GetCount()
294           )
295         );
296     }
297 
298 
299     // Prepare a list of all built-in demos of maxima.
300     {
301       wxFileName demoDir(m_configuration->MaximaShareDir() + "/");
302       demoDir.MakeAbsolute();
303       demoDir.RemoveLastDir();
304       GetDemoFiles_includingSubdirs maximaLispIterator (m_builtInDemoFiles);
305       wxLogMessage(
306         wxString::Format(
307           _("Autocompletion: Scanning %s for loadable demo files."),
308           demoDir.GetFullPath().utf8_str()));
309 
310       wxDir maximadir(demoDir.GetFullPath());
311       if(maximadir.IsOpened())
312         maximadir.Traverse(maximaLispIterator);
313     }
314     wxLogMessage(
315       wxString::Format(
316         _("Found %li demo files."),
317         (unsigned long)m_builtInDemoFiles.GetCount()
318         )
319       );
320     m_builtInLoadFiles.Sort();
321     m_builtInDemoFiles.Sort();
322   }
323 }
324 
UpdateDemoFiles(wxString partial,wxString maximaDir)325 void AutoComplete::UpdateDemoFiles(wxString partial, wxString maximaDir)
326 {
327   #ifdef HAVE_OPENMP_TASKS
328   #pragma omp critical (AutocompleteFiles)
329   #endif
330   {
331     // Remove the opening quote from the partial.
332     if(partial[0] == wxT('\"'))
333       partial = partial.Right(partial.Length()-1);
334 
335     partial.Replace(wxFileName::GetPathSeparator(), "/");
336     int pos;
337     if ((pos = partial.Find(wxT('/'), true)) == wxNOT_FOUND)
338       partial = wxEmptyString;
339     else
340       partial = partial.Left(pos);
341     wxString prefix = partial + wxT("/");
342 
343     // Determine if we need to add the path to maxima's current dir to the path in partial
344     if(!wxFileName(partial).IsAbsolute())
345     {
346       partial = maximaDir + wxFileName::GetPathSeparator() + partial;
347       partial.Replace(wxFileName::GetPathSeparator(), "/");
348     }
349 
350     // Determine the name of the directory
351     if((partial != wxEmptyString) && wxDirExists(partial))
352       partial += "/";
353 
354     // Remove all files from the maxima directory from the demo file list
355     ClearDemofileList();
356 
357     // Add all files from the maxima directory to the demo file list
358     if(partial != wxT("//"))
359     {
360       GetDemoFiles userLispIterator(m_wordList[demofile], prefix);
361       wxDir demofilesdir(partial);
362       if(demofilesdir.IsOpened())
363         demofilesdir.Traverse(userLispIterator);
364     }
365   }
366 }
367 
UpdateGeneralFiles(wxString partial,wxString maximaDir)368 void AutoComplete::UpdateGeneralFiles(wxString partial, wxString maximaDir)
369 {
370   #ifdef HAVE_OPENMP_TASKS
371   #pragma omp critical (AutocompleteFiles)
372   #endif
373   {
374     // Remove the opening quote from the partial.
375     if(partial[0] == wxT('\"'))
376       partial = partial.Right(partial.Length()-1);
377 
378     partial.Replace(wxFileName::GetPathSeparator(), "/");
379     int pos;
380     if ((pos = partial.Find(wxT('/'), true)) == wxNOT_FOUND)
381       partial = wxEmptyString;
382     else
383       partial = partial.Left(pos);
384     wxString prefix = partial + wxT("/");
385 
386     // Determine if we need to add the path to maxima's current dir to the path in partial
387     if(!wxFileName(partial).IsAbsolute())
388     {
389       partial = maximaDir + wxFileName::GetPathSeparator() + partial;
390       partial.Replace(wxFileName::GetPathSeparator(), "/");
391     }
392 
393     // Determine the name of the directory
394     if((partial != wxEmptyString) && wxDirExists(partial))
395       partial += "/";
396 
397     // Add all files from the maxima directory to the demo file list
398     if(partial != wxT("//"))
399     {
400       GetGeneralFiles fileIterator(m_wordList[generalfile], prefix);
401       wxDir generalfilesdir(partial);
402       if(generalfilesdir.IsOpened())
403         generalfilesdir.Traverse(fileIterator);
404     }
405   }
406 }
407 
UpdateLoadFiles(wxString partial,wxString maximaDir)408 void AutoComplete::UpdateLoadFiles(wxString partial, wxString maximaDir)
409 {
410   #ifdef HAVE_OPENMP_TASKS
411   wxLogMessage(_("Starting a background task that scans for autocompletible file names."));
412   #pragma omp critical (AutocompleteFiles)
413   #endif
414   {
415     // Remove the opening quote from the partial.
416     if(partial[0] == wxT('\"'))
417       partial = partial.Right(partial.Length()-1);
418 
419     partial.Replace(wxFileName::GetPathSeparator(), "/");
420     int pos;
421     if ((pos = partial.Find(wxT('/'), true)) == wxNOT_FOUND)
422       partial = wxEmptyString;
423     else
424       partial = partial.Left(pos);
425     wxString prefix = partial + wxT("/");
426 
427     // Determine if we need to add the path to maxima's current dir to the path in partial
428     if(!wxFileName(partial).IsAbsolute())
429     {
430       partial = maximaDir + wxFileName::GetPathSeparator() + partial;
431       partial.Replace(wxFileName::GetPathSeparator(), "/");
432     }
433 
434     // Determine the name of the directory
435     if((partial != wxEmptyString) && wxDirExists(partial))
436       partial += "/";
437 
438     // Remove all files from the maxima directory from the load file list
439     m_wordList[loadfile] = m_builtInLoadFiles;
440 
441     // Add all files from the maxima directory to the load file list
442     if(partial != wxT("//"))
443     {
444       GetMacFiles userLispIterator(m_wordList[loadfile], prefix);
445       wxDir loadfilesdir(partial);
446       if(loadfilesdir.IsOpened())
447         loadfilesdir.Traverse(userLispIterator);
448     }
449   }
450 }
451 
452 /// Returns a string array with functions which start with partial.
CompleteSymbol(wxString partial,autoCompletionType type)453 wxArrayString AutoComplete::CompleteSymbol(wxString partial, autoCompletionType type)
454 {
455   wxArrayString completions;
456   wxArrayString perfectCompletions;
457 
458   #ifdef HAVE_OPENMP_TASKS
459   #pragma omp critical (AutocompleteBuiltins)
460   #pragma omp critical (AutocompleteFiles)
461   #endif
462   {
463     if(
464       ((type == AutoComplete::demofile) || (type == AutoComplete::loadfile)) &&
465       (partial.EndsWith("\""))
466       )
467       partial = partial.Left(partial.Length() - 1);
468 
469     wxASSERT_MSG((type >= command) && (type <= unit), _("Bug: Autocompletion requested for unknown type of item."));
470 
471     if (type != tmplte)
472     {
473       for (size_t i = 0; i < m_wordList[type].GetCount(); i++)
474       {
475         if (m_wordList[type][i].StartsWith(partial) &&
476             completions.Index(m_wordList[type][i]) == wxNOT_FOUND)
477           completions.Add(m_wordList[type][i]);
478       }
479     }
480     else
481     {
482       for (size_t i = 0; i < m_wordList[type].GetCount(); i++)
483       {
484         wxString templ = m_wordList[type][i];
485         if (templ.StartsWith(partial))
486         {
487           if (completions.Index(templ) == wxNOT_FOUND)
488             completions.Add(templ);
489           if (templ.SubString(0, templ.Find(wxT("(")) - 1) == partial &&
490               perfectCompletions.Index(templ) == wxNOT_FOUND)
491             perfectCompletions.Add(templ);
492         }
493       }
494     }
495 
496     // Add a list of words that were definied on the work sheet but that aren't
497     // defined as maxima commands or functions.
498     if (type == command)
499     {
500       WorksheetWords::const_iterator it;
501       for (it = m_worksheetWords.begin(); it != m_worksheetWords.end(); ++it)
502       {
503         if (it->first.StartsWith(partial))
504         {
505           if (completions.Index(it->first) == wxNOT_FOUND)
506           {
507             completions.Add(it->first);
508           }
509         }
510       }
511     }
512 
513     completions.Sort();
514   }
515   if (perfectCompletions.Count() > 0)
516     return perfectCompletions;
517   return completions;
518 }
519 
AddSymbol(wxString fun,autoCompletionType type)520 void AutoComplete::AddSymbol(wxString fun, autoCompletionType type)
521 {
522   #ifdef HAVE_OPENMP_TASKS
523   #pragma omp critical (AutocompleteBuiltins)
524   #endif
525   AddSymbol_nowait(fun, type);
526 }
527 
AddSymbol_nowait(wxString fun,autoCompletionType type)528 void AutoComplete::AddSymbol_nowait(wxString fun, autoCompletionType type)
529 {
530   /// Check for function of template
531   if (fun.StartsWith(wxT("FUNCTION: ")))
532   {
533     fun = fun.Mid(10);
534     type = command;
535   }
536   else if (fun.StartsWith(wxT("TEMPLATE: ")))
537   {
538     fun = fun.Mid(10);
539     type = tmplte;
540   }
541   else if (fun.StartsWith(wxT("UNIT: ")))
542   {
543     fun = fun.Mid(6);
544     type = unit;
545   }
546 
547   /// Add symbols
548   if ((type != tmplte) && m_wordList[type].Index(fun, true, true) == wxNOT_FOUND)
549     m_wordList[type].Add(fun);
550 
551   /// Add templates - for given function and given argument count we
552   /// only add one template. We count the arguments by counting '<'
553   if (type == tmplte)
554   {
555     fun = FixTemplate(fun);
556     wxString funName = fun.SubString(0, fun.Find(wxT("(")));
557     long count = fun.Freq('<');
558     size_t i = 0;
559     for (i = 0; i < m_wordList[type].GetCount(); i++)
560     {
561       wxString t = m_wordList[type][i];
562       if (t.StartsWith(funName) && (t.Freq('<') == count))
563         break;
564     }
565     if (i == m_wordList[type].GetCount())
566       m_wordList[type].Add(fun);
567   }
568 }
569 
570 
FixTemplate(wxString templ)571 wxString AutoComplete::FixTemplate(wxString templ)
572 {
573   templ.Replace(wxT(" "), wxEmptyString);
574   templ.Replace(wxT(",..."), wxEmptyString);
575 
576   /// This will change optional arguments
577   m_args.ReplaceAll(&templ, wxT("<[\\1]>"));
578 
579   return templ;
580 }
581 
582 wxRegEx AutoComplete::m_args("[<\\([^>]*\\)>]");
583