Browse Source

Merge pull request #5196 from vnen/similarity-code-completion

Improve code completion search
Rémi Verschelde 9 years ago
parent
commit
3668768463
5 changed files with 104 additions and 43 deletions
  1. 44 0
      core/ustring.cpp
  2. 2 0
      core/ustring.h
  3. 4 0
      core/variant_call.cpp
  4. 16 0
      doc/base/classes.xml
  5. 38 43
      scene/gui/text_edit.cpp

+ 44 - 0
core/ustring.cpp

@@ -2810,6 +2810,50 @@ bool String::_base_is_subsequence_of(const String& p_string, bool case_insensiti
 	return false;
 	return false;
 }
 }
 
 
+Vector<String> String::bigrams() const {
+	int n_pairs = length() - 1;
+	Vector<String> b;
+	if(n_pairs <= 0) {
+		return b;
+	}
+	b.resize(n_pairs);
+	for(int i = 0; i < n_pairs; i++) {
+		b[i] = substr(i,2);
+	}
+	return b;
+}
+
+// Similarity according to Sorensen-Dice coefficient
+float String::similarity(const String& p_string) const {
+	if(operator==(p_string)) {
+		// Equal strings are totally similar
+		return 1.0f;
+	}
+	if (length() < 2 || p_string.length() < 2) {
+		// No way to calculate similarity without a single bigram
+		return 0.0f;
+	}
+
+	Vector<String> src_bigrams = bigrams();
+	Vector<String> tgt_bigrams = p_string.bigrams();
+
+	int src_size = src_bigrams.size();
+	int tgt_size = tgt_bigrams.size();
+
+	float sum = src_size + tgt_size;
+	float inter = 0;
+	for (int i = 0; i < src_size; i++) {
+		for (int j = 0; j < tgt_size; j++) {
+			if (src_bigrams[i] == tgt_bigrams[j]) {
+				inter++;
+				break;
+			}
+		}
+	}
+
+	return (2.0f * inter)/sum;
+}
+
 static bool _wildcard_match(const CharType* p_pattern, const CharType* p_string,bool p_case_sensitive) {
 static bool _wildcard_match(const CharType* p_pattern, const CharType* p_string,bool p_case_sensitive) {
 	switch (*p_pattern) {
 	switch (*p_pattern) {
 	case '\0':
 	case '\0':

+ 2 - 0
core/ustring.h

@@ -123,6 +123,8 @@ public:
 	bool ends_with(const String& p_string) const;
 	bool ends_with(const String& p_string) const;
 	bool is_subsequence_of(const String& p_string) const;
 	bool is_subsequence_of(const String& p_string) const;
 	bool is_subsequence_ofi(const String& p_string) const;
 	bool is_subsequence_ofi(const String& p_string) const;
+	Vector<String> bigrams() const;
+	float similarity(const String& p_string) const;
 	String replace_first(String p_key,String p_with) const;
 	String replace_first(String p_key,String p_with) const;
 	String replace(String p_key,String p_with) const;
 	String replace(String p_key,String p_with) const;
 	String replacen(String p_key,String p_with) const;
 	String replacen(String p_key,String p_with) const;

+ 4 - 0
core/variant_call.cpp

@@ -249,6 +249,8 @@ static void _call_##m_type##_##m_method(Variant& r_ret,Variant& p_self,const Var
 	VCALL_LOCALMEM1R(String,ends_with);
 	VCALL_LOCALMEM1R(String,ends_with);
 	VCALL_LOCALMEM1R(String,is_subsequence_of);
 	VCALL_LOCALMEM1R(String,is_subsequence_of);
 	VCALL_LOCALMEM1R(String,is_subsequence_ofi);
 	VCALL_LOCALMEM1R(String,is_subsequence_ofi);
+	VCALL_LOCALMEM0R(String,bigrams);
+	VCALL_LOCALMEM1R(String,similarity);
 	VCALL_LOCALMEM2R(String,replace);
 	VCALL_LOCALMEM2R(String,replace);
 	VCALL_LOCALMEM2R(String,replacen);
 	VCALL_LOCALMEM2R(String,replacen);
 	VCALL_LOCALMEM2R(String,insert);
 	VCALL_LOCALMEM2R(String,insert);
@@ -1281,6 +1283,8 @@ _VariantCall::addfunc(Variant::m_vtype,Variant::m_ret,_SCS(#m_method),VCALL(m_cl
 	ADDFUNC1(STRING,BOOL,String,ends_with,STRING,"text",varray());
 	ADDFUNC1(STRING,BOOL,String,ends_with,STRING,"text",varray());
 	ADDFUNC1(STRING,BOOL,String,is_subsequence_of,STRING,"text",varray());
 	ADDFUNC1(STRING,BOOL,String,is_subsequence_of,STRING,"text",varray());
 	ADDFUNC1(STRING,BOOL,String,is_subsequence_ofi,STRING,"text",varray());
 	ADDFUNC1(STRING,BOOL,String,is_subsequence_ofi,STRING,"text",varray());
+	ADDFUNC0(STRING,STRING_ARRAY,String,bigrams,varray());
+	ADDFUNC1(STRING,REAL,String,similarity,STRING,"text",varray());
 
 
 	ADDFUNC2(STRING,STRING,String,replace,STRING,"what",STRING,"forwhat",varray());
 	ADDFUNC2(STRING,STRING,String,replace,STRING,"what",STRING,"forwhat",varray());
 	ADDFUNC2(STRING,STRING,String,replacen,STRING,"what",STRING,"forwhat",varray());
 	ADDFUNC2(STRING,STRING,String,replacen,STRING,"what",STRING,"forwhat",varray());

+ 16 - 0
doc/base/classes.xml

@@ -37268,6 +37268,13 @@ This method controls whether the position between two cached points is interpola
 			Return true if the strings begins with the given string.
 			Return true if the strings begins with the given string.
 			</description>
 			</description>
 		</method>
 		</method>
+		<method name="bigrams">
+			<return type="StringArray">
+			</return>
+			<description>
+			Return the bigrams (pairs of consecutive letters) of this string.
+			</description>
+		</method>
 		<method name="c_escape">
 		<method name="c_escape">
 			<return type="String">
 			<return type="String">
 			</return>
 			</return>
@@ -37627,6 +37634,15 @@ This method controls whether the position between two cached points is interpola
 			<description>
 			<description>
 			</description>
 			</description>
 		</method>
 		</method>
+		<method name="similarity">
+			<return type="float">
+			</return>
+			<argument index="0" name="text" type="String">
+			</argument>
+			<description>
+			Return the similarity index of the text compared to this string. 1 means totally similar and 0 means totally dissimilar.
+			</description>
+		</method>
 		<method name="split">
 		<method name="split">
 			<return type="StringArray">
 			<return type="StringArray">
 			</return>
 			</return>

+ 38 - 43
scene/gui/text_edit.cpp

@@ -3851,8 +3851,14 @@ void TextEdit::undo() {
 		}
 		}
 	}
 	}
 
 
-	cursor_set_line(undo_stack_pos->get().from_line);
-	cursor_set_column(undo_stack_pos->get().from_column);
+	if (undo_stack_pos->get().type == TextOperation::TYPE_REMOVE) {
+		cursor_set_line(undo_stack_pos->get().to_line);
+		cursor_set_column(undo_stack_pos->get().to_column);
+		_cancel_code_hint();
+	} else {
+		cursor_set_line(undo_stack_pos->get().from_line);
+		cursor_set_column(undo_stack_pos->get().from_column);
+	}
 	update();
 	update();
 }
 }
 
 
@@ -3995,28 +4001,19 @@ void TextEdit::set_completion(bool p_enabled,const Vector<String>& p_prefixes) {
 
 
 void TextEdit::_confirm_completion() {
 void TextEdit::_confirm_completion() {
 
 
-	String remaining=completion_current.substr(completion_base.length(),completion_current.length()-completion_base.length());
-	String l = text[cursor.line];
-	bool same=true;
-	//if what is going to be inserted is the same as what it is, don't change it
-	for(int i=0;i<remaining.length();i++) {
-		int c=i+cursor.column;
-		if (c>=l.length() || l[c]!=remaining[i]) {
-			same=false;
-			break;
-		}
-	}
+	begin_complex_operation();
 
 
-	if (same)
-		cursor_set_column(cursor.column+remaining.length());
-	else {
-		insert_text_at_cursor(remaining);
-		if (remaining.ends_with("(") && auto_brace_completion_enabled) {
-			insert_text_at_cursor(")");
-			cursor.column--;
-		}
+	_remove_text(cursor.line, cursor.column - completion_base.length(), cursor.line, cursor.column);
+	cursor_set_column(cursor.column - completion_base.length(), false);
+	insert_text_at_cursor(completion_current);
+
+	if (completion_current.ends_with("(") && auto_brace_completion_enabled) {
+		insert_text_at_cursor(")");
+		cursor.column--;
 	}
 	}
 
 
+	end_complex_operation();
+
 	_cancel_completion();
 	_cancel_completion();
 }
 }
 
 
@@ -4119,30 +4116,29 @@ void TextEdit::_update_completion_candidates() {
 	completion_index=0;
 	completion_index=0;
 	completion_base=s;
 	completion_base=s;
 	int ci_match=0;
 	int ci_match=0;
+	Vector<float> sim_cache;
 	for(int i=0;i<completion_strings.size();i++) {
 	for(int i=0;i<completion_strings.size();i++) {
-		if (completion_strings[i].begins_with(s)) {
+		if (s.is_subsequence_ofi(completion_strings[i])) {
 			// don't remove duplicates if no input is provided
 			// don't remove duplicates if no input is provided
-			if (completion_options.find(completion_strings[i]) != -1 && s != "") {
+			if (s != "" && completion_options.find(completion_strings[i]) != -1) {
 				continue;
 				continue;
 			}
 			}
-			completion_options.push_back(completion_strings[i]);
-			int m=0;
-			int max=MIN(completion_current.length(),completion_strings[i].length());
-			if (max<ci_match)
-				continue;
-			for(int j=0;j<max;j++) {
-
-				if (j>=completion_strings[i].length())
-					break;
-				if (completion_current[j]!=completion_strings[i][j])
-					break;
-				m++;
-			}
-			if (m>ci_match) {
-				ci_match=m;
-				completion_index=completion_options.size()-1;
+			// Calculate the similarity to keep completions in good order
+			float similarity = s.similarity(completion_strings[i]);
+			int comp_size = completion_options.size();
+			if (comp_size == 0) {
+				completion_options.push_back(completion_strings[i]);
+				sim_cache.push_back(similarity);
+			} else {
+				float comp_sim;
+				int pos = 0;
+				do {
+					comp_sim = sim_cache[pos++];
+				} while(pos < comp_size && similarity <= comp_sim);
+				pos--; // Pos will be off by one
+				completion_options.insert(pos, completion_strings[i]);
+				sim_cache.insert(pos, similarity);
 			}
 			}
-
 		}
 		}
 	}
 	}
 
 
@@ -4155,7 +4151,8 @@ void TextEdit::_update_completion_candidates() {
 
 
 	}
 	}
 
 
-	completion_current=completion_options[completion_index];
+	// The top of the list is the best match
+	completion_current=completion_options[0];
 
 
 #if 0	// even there's only one option, user still get the chance to choose using it or not
 #if 0	// even there's only one option, user still get the chance to choose using it or not
 	if (completion_options.size()==1) {
 	if (completion_options.size()==1) {
@@ -4167,8 +4164,6 @@ void TextEdit::_update_completion_candidates() {
 
 
 	}
 	}
 #endif
 #endif
-	if (completion_options.size()==1 && s==completion_options[0])
-		_cancel_completion();
 
 
 	completion_enabled=true;
 	completion_enabled=true;
 }
 }